code:
/**
 * Copyright (c) 2012-2013 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.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Font;
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.gestures.DoubleTapDetector;
import com.nokia.example.amaze.gestures.SafeGestureEvent;
import com.nokia.example.amaze.gestures.SafeGestureInteractiveZone;
import com.nokia.example.amaze.gestures.SafeGestureListener;
import com.nokia.example.amaze.gestures.SafeGestureRegistrationManager;
import com.nokia.example.amaze.model.MarbleModel;
import com.nokia.example.amaze.model.Maze;
import com.nokia.example.amaze.model.MyTimer;
import com.nokia.example.amaze.sensors.AccelerationProvider;

/**
 * The main UI class.
 */
public class MazeCanvas
    extends GameCanvas
    implements Runnable,
               CommandListener,
               OrientationListener,
               CameraAnimator.Listener,
               AccelerationProvider.Listener,
               DoubleTapDetector.Listener,
               SafeGestureListener,
               Menu.Listener
{
    // Constants

    /* Ticks coefficient is used to define the normal speed of the game
     * regardless of the performance of the phone i.e. it is used to normalise
     * the changes to the model. In this case it can be understood as the
     * desired frame rate (or the duration of a single frame to be more
     * precise).
     */
    public static final float TICKS_COEFFICIENT = 1000.0f / 30.0f;

    private static final int MILLIS_TO_SLEEP = 5; // Milliseconds to wait in the main loop

    public static final int MAZE_CORRIDOR_COUNT = 10;
    public static final float MAZE_SIDE_LENGTH = 200f; // Defines the overall size of the maze
    private static final float WALL_HEIGHT = 10.f; // Height of the walls
    private static final int POSITION_TOLERANCE = 12;
    private static final float DEFAULT_STEP_LENGTH_PER_AXIS = 3.0f; // Amount to advance on each step

    public static final float ACCELERATION_THRESHOLD = 1.5f;
    public static final float ACCELERATION_COEFFICIENT_X = 1.5f;
    public static final float ACCELERATION_COEFFICIENT_Y = 0.3f;
    private static final int MIN_CALIBRATION_VALUE = -6;
    public static final int UNDEFINED = -20;
    private static final int ZOOM_INTERVAL = 10;

    private static final float DEFAULT_TOP_CAMERA_ANGLE = 75f;
    private static final float PAUSE_CAMERA_ANGLE = 45f;
    private static final float DEFAULT_ZOOM = 1.0f;
    private static final int MIN_ZOOM = -70; // As close as you can zoom
    private static final int MAX_ZOOM = -MIN_ZOOM; // As far as you can zoom
    private static final float MAX_TILT_COEFFICIENT = 3.0f;
    private static final float TILT_THRESHOLD = 0.1f;
    private static final int LEVEL_TIME = 60; // Initial level time, in seconds
    private static final int MIN_FPS_STATS_COUNT = 20;

    // Game states
    public static final int NOT_STARTED = 0; // When in start menu
    public static final int TRANSITION_ANIMATION_ONGOING = 1;
    public static final int ONGOING = 2;
    public static final int PAUSED = 3;
    public static final int LEVEL_FINISHED = 4;
    public static final int GAME_OVER = 5;

    private static final String RESUME_MENU_ITEM_TEXT = "Resume";
    private static final String SET_BACKGROUND_PREFIX_MENU_ITEM_TEXT = "Set background ";
    private static final String SWITCH_DEBUG_MODE_PREFIX_MENU_ITEM_TEXT = "Switch debug mode ";
    private static final String RESTART_GAME_MENU_ITEM_TEXT = "Restart game";

    public static final int TEXT_COLOR = 0xff81daff;
    public static final int MARGIN = 5;

    // 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 Maze _maze = null;
    private final MarbleModel _marbleModel;
    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 DoubleTapDetector _doubleTapDetector = null;
    private final Command _exitAndBackCommand = new Command("", Command.EXIT, 1);
    private double _calibration = UNDEFINED;
    private double _ax;
    private double _ay;
    private boolean _goalReached = false;
    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;
    private volatile int _loopCounter = 1;
    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;
        _marbleModel = new MarbleModel();
        setCommandListener(this);
    }

    /**
     * Initializes the game canvas.
     */
    public void init() {
        setFullScreenMode(true);

        _width = getWidth();
        _height = getHeight();

        _maze = new Maze(MAZE_CORRIDOR_COUNT, MAZE_SIDE_LENGTH, WALL_HEIGHT);
        _marbleModel.setPosition(0f, 2 * WALL_HEIGHT + 3f, 0f);

        _doubleTapDetector = new DoubleTapDetector(this);

        // Register to listen to the pinch event
        // Note: If the device doesn't have multitouch support, this will cause
        // unhandled exception.
        SafeGestureRegistrationManager.setListener(this, this);
        SafeGestureInteractiveZone gestureZone = new SafeGestureInteractiveZone();
        gestureZone.setGesture(SafeGestureInteractiveZone.GESTURE_PINCH);
        gestureZone.setRectangle(0, 0, _width, _height);
        SafeGestureRegistrationManager.register(this, gestureZone);

        _topCamera = new Camera();
        _povCamera = new Camera();

        _menu = new Menu(this, _width, _height);

        // Get the Graphics3D instance
        _graphics3d = Graphics3D.getInstance();

        // Create buttons
        if (!Main.HAS_ONE_KEY_BACK) {
            _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);

        AccelerationProvider.getProvider(this);
        Orientation.addOrientationListener(this);

        showStartMenu();
    }

    /**
     * This method implements main (game) loop.
     * @see java.lang.Runnable#run()
     */
    public void run() {
        _graphics = getGraphics();
        long tempTime = 0;
        long ticks = 0;

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

            if (isShown()) {
                MyTimer.instance().update(time);

                if (_gameState == ONGOING) {
                    _timeUsed = time - _levelStartTime;

                    if (_timeToBeat - _timeUsed <= 0) {
                        // Out of time! End the game.
                        endGame();
                    }

                    updateModel((int)ticks);
                    updateCamera();

                    if (time - tempTime > 5000) {
                        DeviceControl.setLights(0, 100); // Prevent screen lock
                        tempTime = time;
                    }
                } // if (_gameState == ONGOING)

                // Camera animator is updated all the time if running
                if (_cameraAnimator.running()) {
                    _cameraAnimator.update((int)ticks);

                    if (_cameraAnimator.animationType() ==
                            CameraAnimator.PAUSE_ANIMATION
                            && time - tempTime > 20)
                    {
                        shiftBackground(-1);
                        tempTime = time;
                    }
                }

                // Tilting the board does not need to be done very often
                if (_loopCounter % 3 == 0) {
                    tiltBoard();
                }

                // Update the loop counter
                if (_loopCounter > 9) {
                    _loopCounter = 1;
                }
                else {
                    _loopCounter++;
                }

                // Draw the graphics
                draw3D(_graphics, (int)ticks);
                draw2D(_graphics);

                // Flush the buffer to the screen
                flushGraphics();
            } // if (isShown())

            // Wait for a little and give the other threads the chance to run
            try {
                Thread.sleep(MILLIS_TO_SLEEP);
            }
            catch (InterruptedException e) {
            }

            ticks = System.currentTimeMillis() - time;

            if (isShown() && (_debugMode || _fpsCount < MIN_FPS_STATS_COUNT)) {
                // Calculate the FPS
                _fps = ticks > 0 ? 1000 / ticks : 60;

                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 < 20) {
                                // Remove the background to boost the performance
                                setBackground(false);
                            }

                            _firstTime = false;
                        }
                    }
                }
            } // if (_debugMode || _fpsCount < MIN_FPS_STATS_COUNT)
        } // while (_running)
    }

    /**
     * Updates the model i.e. calculates the new position of the marble.
     */
    private final void updateModel(final int ticks) {
        if (_povMode) {
            moveMarbleInPovMode();
        }
        else {
            // Update the marble position
            float[] position = _marbleModel.position();
            _marble.setTranslation(position[0] + MarbleModel.DEFAULT_SIZE,
                                   position[1],
                                   position[2] + MarbleModel.DEFAULT_SIZE);
            moveMarble(ticks);
        }

        // This does not need to be done very so often
        if (_loopCounter == 9) {
            // Check if the marble reached the end
            if (!_goalReached) {
                final float[] position = _marbleModel.position();

                if (_maze.isAtTheEnd(position[0], position[2], POSITION_TOLERANCE)) {
                    // We're there!
                    _goalReached = true;
                    finishLevel();
                }
            }
        }
    }

    //-------------------------------------------------------------------------
    // UI interaction implementation ->
    //-------------------------------------------------------------------------

    /**
     * @see javax.microedition.lcdui.Canvas#pointerPressed(int, int)
     */
    protected void pointerPressed(int x, int y) {
        if (_infoDialog != null) {
            return;
        }

        if (TipBox.visible()) {
            TipBox.hide();
            return;
        }

        if (_gameState == MazeCanvas.NOT_STARTED
                || _gameState == MazeCanvas.PAUSED
                || _gameState == MazeCanvas.GAME_OVER)
        {
            if (!Main.HAS_ONE_KEY_BACK && x > _width - 42 && y > _height - 42) {
                // Exit button pressed
                setButtonPressed(IconButton.EXIT);
            }
            else if (x < 42 && y > _height - 42) {
                // Info button pressed
                setButtonPressed(IconButton.INFO);
            }
            else {
                switch (_gameState) {
                case NOT_STARTED: {
                    startNewGame();
                    break;
                }
                case PAUSED: {
                    _menu.onPressed(x, y);
                    break;
                }
                case GAME_OVER: {
                    _cameraAnimator.startAnimation(
                        CameraAnimator.LEVEL_RESET_ANIMATION_STEP);
                    break;
                }
                }
            }

            return;
        }

        if (_doubleTapDetector.onTapped(x, y)) {
            // Double tap was detected
            return;
        }

        if (x > _width - 42 && y > _height - 42) {
            // Pause button pressed
            setButtonPressed(IconButton.PAUSE);
        }
        else if (x < 42 && y > _height - 42) {
            // View mode button pressed
            // Do nothing yet
        }
    }

    /**
     * @see javax.microedition.lcdui.Canvas#pointerReleased(int, int)
     */
    protected void pointerReleased(int x, int y) {
        if (_gameState == NOT_STARTED
                || _gameState == PAUSED
                || _gameState == GAME_OVER)
        {
            if (_infoDialog != null) {
                // Info dialog is visible
                if (y > _infoDialog.linkTextY() - MARGIN
                    && y < _infoDialog.linkTextY()
                        + _infoDialog.linkTextHeight() + MARGIN)
                {
                    Main.openLink(InfoDialog.LINK_URL);
                }
                else {
                    _infoDialog = null;

                    if (Main.HAS_ONE_KEY_BACK && _gameState == PAUSED) {
                        removeCommand(_exitAndBackCommand);
                    }
                }

                return;
            }

            if (!Main.HAS_ONE_KEY_BACK
                && _iconButtons[IconButton.EXIT].pressed()
                && x > _width - 42 && y > _height - 42)
            {
                // Exit button was tapped, do quit
                _midlet.quitApp();
            }
            else if (_iconButtons[IconButton.INFO].pressed()
                     && x < 42 && y > _height - 42)
            {
                // Show info dialog
                if (_infoDialog == null) {
                    _infoDialog = new InfoDialog(_midlet);

                    if (Main.HAS_ONE_KEY_BACK) {
                        addCommand(_exitAndBackCommand);
                    }
                }
            }
            else {
                _menu.onReleased(x, y);
            }

            setButtonPressed(UNDEFINED);
            return;
        }

        if (_gameState == ONGOING) {
            if (_iconButtons[IconButton.PAUSE].pressed()
                && x > _width - 42 && y > _height - 42)
            {
                // Do pause
                pause();
            }
            else if (x < 42 && y > _height - 42)
            {
                // Toggle view mode
                toggleViewMode();
            }
        }

        setButtonPressed(UNDEFINED);
    }

    /**
     * @see javax.microedition.lcdui.Canvas#pointerDragged(int, int)
     */
    protected void pointerDragged(int x, int y) {
        if (_gameState == PAUSED && _infoDialog != null) {
            _menu.onPressed(x, y);
        }
    }

    /**
     * @see AccelerationProvider.Listener#onDataReceived(double, double, double)
     */
    public void onDataReceived(double ax, double ay, double az) {
        if (_gameState != ONGOING) {
            return;
        }

        if (_calibration == UNDEFINED) {
            // Do calibrate now based on the current reading
            if (_isPortrait) {
                setCalibration(-ay);
            }
            else {
                setCalibration(-ax);
            }
        }

        if (_isPortrait) {
            _ax = ax;
            _ay = ay + _calibration;
        }
        else {
            _ax = ax + _calibration;
            _ay = ay;
        }
    }

    /**
     * Reset the zoom. If the current zoom is the default, the previous zoom
     * level is restored.
     *
     * @see DoubleTapDetector.Listener#onDoubleTapDetected()
     */
    public void onDoubleTapDetected() {
        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.onDoubleTapDetected(): Resetting the default zoom.");
            _previousZoom = _zoom;
            _zoom = DEFAULT_ZOOM;
        }
        else {
            // Restore the previous zoom
            System.out.println("MazeCanvas.onDoubleTapDetected(): Restoring the previous zoom.");
            _zoom = _previousZoom;
        }

        calculateRelativeZoomAndCameraOffset();
        updateCamera();
    }

    /**
     * From SafeGestureListener.
     *
     * Handles pinch gestures and changes the distance of the camera based on
     * the gesture.
     *
     * @see SafeGestureListener#gestureAction(java.lang.Object,
     * SafeGestureInteractiveZone, SafeGestureEvent)
     */
    public void gestureAction(Object container,
                              SafeGestureInteractiveZone gestureInteractiveZone,
                              SafeGestureEvent gestureEvent)
    {
        if (!_povMode && gestureEvent.getType() == SafeGestureInteractiveZone.GESTURE_PINCH) {
            if (gestureEvent.getPinchDistanceChange() < 0) {
                // If the gesture was inwards, scale smaller
                doZoom(ZOOM_INTERVAL);
            }
            else if (gestureEvent.getPinchDistanceChange() > 0) {
                // If the gesture was outwards, scale larger
                doZoom(-ZOOM_INTERVAL);
            }
        }
    }

    /**
     * @see Menu.Listener#onMenuItemSelected(int)
     */
    public void onMenuItemSelected(int index, MenuItem item) {
        if (_gameState == MazeCanvas.PAUSED && item != null) {
            final String text = item.text();

            if (text.equals(RESUME_MENU_ITEM_TEXT)) {
                // Resume
                resume();
            }
            else if (text.startsWith(SET_BACKGROUND_PREFIX_MENU_ITEM_TEXT)) {
                // Toggle background
                setBackground(!_hasBackground);
                item.setText(SET_BACKGROUND_PREFIX_MENU_ITEM_TEXT
                             + (_hasBackground ? "off" : "on"));
            }
            else if (text.startsWith(SWITCH_DEBUG_MODE_PREFIX_MENU_ITEM_TEXT)) {
                // Toggle debug mode
                _debugMode = !_debugMode;
                item.setText(SWITCH_DEBUG_MODE_PREFIX_MENU_ITEM_TEXT
                             + (_debugMode ? "off" : "on"));
            }
            else if (text.equals(RESTART_GAME_MENU_ITEM_TEXT)) {
                // Restart the game
                startNewGame();
            }
        }
    }

    /**
     * Sets the accelerometer calibration value for the Y axis.
     * @param calibrationY The value to set.
     */
    public void setCalibration(final double calibration) {
        _calibration = calibration;

        if (_calibration < MIN_CALIBRATION_VALUE
                && _calibration != UNDEFINED)
        {
            _calibration = MIN_CALIBRATION_VALUE;
        }
    }

    /**
     * Resets the input state.
     */
    public void resetMarbleVelocity() {
        _marbleModel.setVelocity(new float[] { 0f, 0f, 0f });
    }

    //-------------------------------------------------------------------------
    // <- UI interaction implementation
    //-------------------------------------------------------------------------

    /*
     * @see javax.microedition.lcdui.CommandListener#commandAction(javax.microedition.lcdui.Command, javax.microedition.lcdui.Displayable)
     */
    public void commandAction(Command command, Displayable displayble) {
        if (command == _exitAndBackCommand) {
            switch (_gameState) {
            case NOT_STARTED:
                if (_infoDialog != null) {
                    // Hide the info dialog
                    _infoDialog = null;
                }
                else {
                    _midlet.quitApp();
                }

                break;
            case ONGOING:
                pause();
                break;
            case PAUSED:
                if (_infoDialog != null) {
                    // Hide the info dialog
                    _infoDialog = null;
                    removeCommand(_exitAndBackCommand);
                }

                break;
            case GAME_OVER:
                _midlet.quitApp();
                break;
            default:
                break;
            }
        }
    }

    /**
     * @see com.nokia.mid.ui.orientation.OrientationListener#displayOrientationChanged(int)
     */
    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());
            setCalibration(UNDEFINED);
        }
    }

    /**
     * A lot of the transition logic is done here.
     * @see com.nokia.example.amaze.ui.CameraAnimator.Listener#onAnimationFinished(int)
     */
    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();
            }
        }
    }

    /**
     * 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;
    }

    /**
     * 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 * MAZE_SIDE_LENGTH);
        _topCamera.setTransform(transform);
        updateCamera();

        // Start the pause animation
        _cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION);

        if (Main.HAS_ONE_KEY_BACK) {
            addCommand(_exitAndBackCommand);
        }
    }

    /**
     * Starts a new game.
     */
    public void startNewGame() {
        _goalReached = 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;
        }

        resetMarbleVelocity();
        TipBox.showTips(_height); // This only works when called the first time

        if (Main.HAS_ONE_KEY_BACK) {
            addCommand(_exitAndBackCommand);
        }
    }

    /**
     * Pauses the game.
     */
    public void pause() {
        if (_gameState == PAUSED) {
            // Already paused
            return;
        }

        // Create the pause menu
        _menu.clear();
        _menu.addItem(RESUME_MENU_ITEM_TEXT);
        _menu.addItem(SET_BACKGROUND_PREFIX_MENU_ITEM_TEXT
                      + (_hasBackground ? "off" : "on"));
        _menu.addItem(SWITCH_DEBUG_MODE_PREFIX_MENU_ITEM_TEXT
                      + (_debugMode ? "off" : "on"));
        _menu.addItem(RESTART_GAME_MENU_ITEM_TEXT);

        Transform targetTransform = new Transform();
        targetTransform.postRotate(PAUSE_CAMERA_ANGLE, -1f, 0f, 0f);
        targetTransform.postTranslate(0f, 0f, DEFAULT_ZOOM * MAZE_SIDE_LENGTH);

        _cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION,
                                          targetTransform, null);

        // Resume will result in re-calibration
        setCalibration(UNDEFINED);

        // Update the game state
        _gameState = PAUSED;

        if (Main.HAS_ONE_KEY_BACK) {
            removeCommand(_exitAndBackCommand);
        }
    }

    /**
     * Resumes the current game.
     */
    public void resume() {
        System.out.println("MazeCanvas.resume()");

        if (_gameState == PAUSED) {
            _levelStartTime = System.currentTimeMillis() - _timeUsed;
            _cameraAnimator.stopAnimation();

            if (Main.HAS_ONE_KEY_BACK) {
                addCommand(_exitAndBackCommand);
            }
        }
        else {
            System.out.println("MazeCanvas.resume(): No game to resume!");
        }
    }

    /**
     * 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);

        if (Main.HAS_ONE_KEY_BACK) {
            addCommand(_exitAndBackCommand);
        }
    }

    /**
     * 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);
    }

    /**
     * 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();
    }

    /**
     * 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();
    }

    /**
     * Sets the button with the given ID in pressed state. If the ID does not
     * match any of the buttons, all buttons are set in unpressed state.
     * @param id The ID of the buttonn.
     */
    public void setButtonPressed(int id) {
        if (!Main.HAS_ONE_KEY_BACK) {
            _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);
        }
    }

    /**
     * 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);
        }
    }

    /**
     * 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);
        }
    }

    /**
     * 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) {
        // First check if the target is inside the maze area
        float mazeSize = 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;
        }

        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;
    }

    /**
     * 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()");
        _goalReached = 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();
        resetMarbleVelocity();
    }

    /**
     * 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, 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)(-_ax);
        final float tiltY = (float)(-_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() {
        // Check if we should be turning to some direction
        if (Math.abs(_ax) > ACCELERATION_THRESHOLD) {
            _povAngleY += (float)_ax * ACCELERATION_COEFFICIENT_X;
            shiftBackground((int)(-_ax * ACCELERATION_COEFFICIENT_X) * 3);
        }

        // Check if we should be moving back or forth
        final float absCalibratedAy =(float)Math.abs(_ay);

        if (absCalibratedAy > ACCELERATION_THRESHOLD) {
            final int mark = (_ay > 0) ? 1 : -1;

            final float stepLength =
                    DEFAULT_STEP_LENGTH_PER_AXIS
                    * (absCalibratedAy * 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 float[] position = _marbleModel.position();
            final int collision =
                marbleCollidesAt(position[0] + stepX, position[2] + 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.
     *
     * @param ticks The milliseconds since last time the method was called.
     */
    private final void moveMarble(final int ticks) {
        // Calculate the velocity
        float[] velocity = _marbleModel.calculateVelocity(_ax, _ay, ticks);

        // Check for collisions
        int collision = -1;
        int safety = 0; // To prevent forever loop
        final float[] position = _marbleModel.position();
        final float ticksCoefficient = (float)ticks / MazeCanvas.TICKS_COEFFICIENT;

        while (collision != 0 && safety < 4) {
            collision = marbleCollidesAt(
                position[0] + velocity[0] * ticksCoefficient,
                position[2] + velocity[2] * ticksCoefficient);

            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(ticks);
    }

    /**
     * Draws 2D graphics e.g. information on top of the screen.
     * @param graphics
     */
    private void draw2D(Graphics graphics) {
        graphics.setColor(TEXT_COLOR);
        final Font defaultFont = Font.getDefaultFont();
        graphics.setFont(defaultFont);

        if (_debugMode) {
            // Draw the accelerometer sensor readings in the bottom
            String ax = String.valueOf(_ax);
            String ay = String.valueOf(_ay);
            ax = ax.length() > 4 ? ax.substring(0, 4) : ax;
            ay = ay.length() > 4 ? ay.substring(0, 4) : ay;
            graphics.drawString("A: [" + ax + ", " + ay + "]",
                    42, _height - defaultFont.getHeight(),
                    Graphics.BOTTOM | Graphics.LEFT);

            // Draw the FPS
            graphics.drawString("FPS: " + _fps + " (" +
                    ((_fpsCount < MIN_FPS_STATS_COUNT) ? -1 : _averageFps) + ")",
                    42, _height, 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);

                if (!Main.HAS_ONE_KEY_BACK) {
                    _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!"
        }
    }

    /**
     * Renders the 3D objects of the scene.
     * @param graphics The Graphics instance.
     * @param ticks The time elapsed since last rendering in milliseconds.
     */
    private final void draw3D(final Graphics graphics, final int ticks) {
        boolean bound = false;

        try {
            // Bind the target
            _graphics3d.bindTarget(graphics);
            bound = true;

            // Advance the animation
            _world.animate(ticks);

            // Do the rendering
            _graphics3d.render(_world);
        }
        finally {
            // Release the target
            if (bound) {
                _graphics3d.releaseTarget();
            }
        }
    }
}


code:
/**
 * Copyright (c) 2012-2013 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.lcdui.Font;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

import com.nokia.example.amaze.Main;

/**
 * A generic game menu.
 */
public class Menu {
    // Constants
    private static final int MAX_MENU_ITEM_COUNT = 4;

    // Members
    private Listener _listener = null;
    private MenuItem[] _items = null;
    private Font _font = null;
    private final Image _itemBackgroundImage = Main.makeImage("/graphics/menu-item-bg.png");
    private int _width;
    private int _height;
    private int _fontHeight;
    private int _pressedIndex = -1;

    /**
     * Constructor.
     * @param width
     * @param height
     */
    public Menu(Listener listener, int width, int height) {
        _items = new MenuItem[MAX_MENU_ITEM_COUNT];
        setSize(width, height);
        _listener = listener;
        _font = Font.getDefaultFont();
        _fontHeight = _font.getHeight();
    }

    /**
     * Sets the menu size.
     * @param width
     * @param height
     */
    public void setSize(int width, int height) {
        _width = width;
        _height = height;
    }

    /**
     * Adds a new item to the menu with the given text.
     * @param text The text of the menu item.
     * @return True if the menu item was added successfully, false otherwise.
     */
    public boolean addItem(String text) {
        for (int i = 0; i < MAX_MENU_ITEM_COUNT; ++i) {
            if (_items[i] == null) {
                MenuItem item = new MenuItem(text);
                _items[i] = item;
                return true;
            }
        }

        return false;
    }

    /**
     * For convenience.
     * Adds the given menu item to the menu.
     * @param item The item to add.
     * @return True if the menu item was added successfully, false otherwise.
     */
    public boolean addItem(MenuItem item) {
        for (int i = 0; i < MAX_MENU_ITEM_COUNT; ++i) {
            if (_items[i] == null) {
                _items[i] = item;
                return true;
            }
        }

        return false;
    }

    /**
     * Returns a menu item at the given index or null if not found.
     * @param index The index of the menu item.
     * @return A menu item or null.
     */
    public MenuItem itemAt(final int index) {
        if (_items != null && index >= 0 && index < menuItemCount()) {
            return (MenuItem)_items[index];
        }

        return null;
    }

    /**
     * Clears all menu items.
     */
    public void clear() {
        for (int i = 0; i < MAX_MENU_ITEM_COUNT; ++i) {
            _items[i] = null;
        }
    }

    /**
     * Handles "on pressed" events.
     * @param x
     * @param y
     */
    public void onPressed(final int x, final int y) {
        final int count = menuItemCount();
        final int startY = _height / 2 - count * MenuItem.MENU_ITEM_HEIGHT / 2;

        if (y < startY || y > count * MenuItem.MENU_ITEM_HEIGHT + startY) {
            setPressed(_pressedIndex, false);
            _pressedIndex = -1;
            return;
        }

        int prevPressedIndex = _pressedIndex;
        _pressedIndex = (y - startY) / MenuItem.MENU_ITEM_HEIGHT;

        if (prevPressedIndex != _pressedIndex) {
            setPressed(prevPressedIndex, false);
        }

        if (!setPressed(_pressedIndex, true)) {
            _pressedIndex = -1;
        }
    }

    /**
     * Handles "on released" events.
     * @param x
     * @param y
     */
    public void onReleased(final int x, final int y) {
        final int count = menuItemCount();
        final int startY = _height / 2 - count * MenuItem.MENU_ITEM_HEIGHT / 2;

        if (y < startY || y > count * MenuItem.MENU_ITEM_HEIGHT + startY) {
            setPressed(_pressedIndex, false);
            _pressedIndex = -1;
            return;
        }

        if (_listener != null
            && _pressedIndex == (y - startY) / MenuItem.MENU_ITEM_HEIGHT)
        {
            System.out.println("Menu::onReleased(): " + _pressedIndex);
            _listener.onMenuItemSelected(_pressedIndex, _items[_pressedIndex]);
        }

        setPressed(_pressedIndex, false);
        _pressedIndex = -1;
    }

    /**
     * Paints the menu.
     * @param graphics
     */
    public void paint(Graphics graphics) {
        final int count = menuItemCount();
        MenuItem item = null;
        String text = null;
        int y = _height / 2 - count * MenuItem.MENU_ITEM_HEIGHT / 2;

        for (int i = 0; i < count; ++i) {
            item = (MenuItem)_items[i];
            text = item.text();

            if (text == null) {
                continue;
            }

            if (item.pressed()) {
                graphics.setColor(MenuItem.COLOR);
                graphics.fillRect(0, y, _width, MenuItem.MENU_ITEM_HEIGHT);
                graphics.setColor(0x00000000);
            }
            else {
                graphics.drawImage(_itemBackgroundImage, 0, y,
                                   Graphics.TOP | Graphics.LEFT);
                graphics.setColor(MenuItem.COLOR);
            }

            graphics.drawString(text, (_width - _font.stringWidth(text)) / 2,
                    (y + MenuItem.MENU_ITEM_HEIGHT / 2) - _fontHeight / 2,
                    Graphics.TOP | Graphics.LEFT);
            y += MenuItem.MENU_ITEM_HEIGHT;
        }
    }

    /**
     * @return The number of items in the menu.
     */
    private final int menuItemCount() {
        int retval = 0;

        for (int i = 0; i < MAX_MENU_ITEM_COUNT; ++i) {
            if (_items[i] != null) {
                retval++;
            }
            else {
                break;
            }
        }

        return retval;
    }

    /**
     * Sets the menu item with the given index pressed/unpressed.
     * @param index The index of a menu item-
     * @param pressed Whether to item should be pressed or unpressed.
     * @return True if the pressed property of the item was set successfully,
     *         false otherwise.
     */
    private boolean setPressed(int index, boolean pressed) {
        if (index >= 0 && index < menuItemCount()) {
            MenuItem item = (MenuItem)_items[index];

            if (item != null && !item.disabled()) {
                item.setPressed(pressed);
                return true;
            }
        }

        return false;
    }

    /**
     * Menu listener interface.
     */
    public interface Listener {
        /**
         * Notification for menu item selected event.
         * @param index The index of the item in the collection.
         * @param item The selected menu item instance.
         */
        void onMenuItemSelected(int index, MenuItem item);
    }
}

repo_name: snapshot/amaze

license: unknown


code:
/**
 * Copyright (c) 2012-2013 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 com.nokia.example.amaze.model.MyTimer;

/**
 * A menu item that can be added to menu implemented in Menu.java.
 */
public class MenuItem implements MyTimer.Listener {
    // Constants
    public static final int MENU_ITEM_HEIGHT = 50;
    public static final int COLOR = MazeCanvas.TEXT_COLOR;
    public static final int BLINK_INTEVAL = 500; // Milliseconds

    // Members
    private String _text = null;
    private boolean _pressed = false;
    private boolean _disabled = false;

    /**
     * Constructor.
     * @param text
     */
    public MenuItem(String text) {
        _text = text;
    }

    /**
     * From MyTimer.Listener.
     * Makes the menu item blink by changing the state between pressed and
     * not pressed.
     */
    public void onTimeout() {
        _pressed = !_pressed;
    }

    /**
     * @param text The text to set for this item.
     */
    public void setText(final String text) {
        _text = text;
    }

    /**
     * @return The text of this item.
     */
    public final String text() {
        return _text;
    }

    /**
     * @param pressed
     */
    public void setPressed(boolean pressed) {
        _pressed = pressed;
    }

    /**
     * @return True if the item is pressed, false otherwise.
     */
    public final boolean pressed() {
        return _pressed;
    }

    /**
     * @param disabled If true, will disable the item.
     */
    public void setDisabled(boolean disabled) {
        _disabled = disabled;
    }

    /**
     * @return True if the item is disabled, false otherwise.
     */
    public final boolean disabled() {
        return _disabled;
    }
}


code:
/**
 * Copyright (c) 2003-2013 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.m3g.IndexBuffer;
import javax.microedition.m3g.Mesh;
import javax.microedition.m3g.Transform;
import javax.microedition.m3g.TriangleStripArray;
import javax.microedition.m3g.VertexArray;
import javax.microedition.m3g.VertexBuffer;

public class Plane
{
  // define a square in plane xy
  private final static short POINTS[] = new short[]
  {
    (short) 1, (short) 1, (short) 0,
    (short) 1, (short) -1, (short) 0,
    (short) -1, (short) 1, (short) 0,
    (short) -1, (short) -1, (short) 0,
  };

  // define the texture coordinates
  private final static short TEXTCOORDINATES[] = new short[]
  {
    (short) 1, (short) 1,
    (short) 0, (short) 1,
    (short) 1, (short) 0,
    (short) 0, (short) 0,
  };

  // triangle strip indices
  private final static int INDICES[] =
  {
    2, 3, 0,
    1, 0, 3,
    0, 3, 2,
    3, 0, 1
  };

  // strip lengths
  private final static int[] LENGTHS = new int[] {3, 3, 3, 3};

  // these arrays are the same for each plane
  private final static VertexArray POSITIONS_ARRAY, TEXTURE_ARRAY;
  private final static IndexBuffer INDEX_BUFFER;

  private Transform wallTransform = new Transform();
  private float textureRepeat;

  static
  {
    // initialize the common arrays
    POSITIONS_ARRAY = new VertexArray(POINTS.length / 3, 3, 2);
    POSITIONS_ARRAY.set(0, POINTS.length / 3, POINTS);
    TEXTURE_ARRAY = new VertexArray(TEXTCOORDINATES.length / 2, 2, 2);
    TEXTURE_ARRAY.set(0, TEXTCOORDINATES.length / 2, TEXTCOORDINATES);
    INDEX_BUFFER = new TriangleStripArray(INDICES, LENGTHS);
  }

  // Builds a new plane with a given transform)
  // and the texture repeated n times
  public Plane(Transform wallTransform, float textureRepeat)
  {
    this.wallTransform = wallTransform;
    this.textureRepeat = textureRepeat;
  }

  // Build the mesh
  Mesh createMesh()
  {
    VertexBuffer vertexBuffer = new VertexBuffer();
    vertexBuffer.setPositions(POSITIONS_ARRAY, 1.0f, null);
    vertexBuffer.setTexCoords(0,
                              TEXTURE_ARRAY,
                              (float) textureRepeat, null);

    Mesh mesh = new Mesh(vertexBuffer, INDEX_BUFFER, null);
    mesh.setTransform(wallTransform);
    return mesh;
  }

}


code:
/**
 * Copyright (c) 2012-2013 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.lcdui.Font;

/**
 * Utility class for text word wrapping with Graphics.drawString().
 */
public class TextWrapper {
    // Constants
    private char LINE_BREAK = '\n';
    private char WHITE_SPACE = ' ';

    // Members
    static private TextWrapper _instance = null;
    private String _text = null;
    private Font _font;
    private int _fontHeight;
    private int _totalLength = 0;
    private int _width = 0;
    private int _start = 0;
    private int _position = 0;

    /**
     * Getters for the singleton instance.
     * @param width The width of the text area.
     */
    static public TextWrapper instance(final Font font, final int width) {
        if (_instance == null) {
            _instance = new TextWrapper(font, width);
        }
        else {
            _instance.setFont(font);
            _instance.setWidth(width);
        }

        return _instance;
    }

    /**
     * Constructor.
     * @param width The width of the text area.
     */
    private TextWrapper(Font font, int width) {
        if (font == null || width <= 0) {
            throw new IllegalArgumentException();
        }

        setFont(font);
        setWidth(width);
    }

    /**
     * Sets the font. Used for determine the text size.
     * @param font The font to set.
     */
    public void setFont(final Font font) {
        if (font != null && _font != font) {
            _font = font;
            _fontHeight = _font.getHeight();
        }
    }

    /**
     * Sets the width of the text area so that this wrapper knows where to cut
     * the line.
     * @param width The width of the text area.
     */
    public void setWidth(final int width) {
        if (width > 0) {
            _width = width;
        }
    }

    /**
     * @param text The text to wrap.
     */
    public void setText(String text) {
        if (text == null) {
            throw new IllegalArgumentException();
        }

        _text = text;
        _totalLength = _text.length();
        _start = 0;
        _position = 0;
    }

    /**
     * @return The text set currently.
     */
    public String text() {
        return _text;
    }

    /**
     * @return The length of the given line.
     */
    public final int lineWidth(final String line) {
        return _font.stringWidth(line);
    }

    /**
     * @return The height of a single text line.
     */
    public final int lineHeight() {
        return _fontHeight;
    }

    /**
     * @return The number of lines the current text will be divided to.
     */
    public int lineCount() {
        if (_text == null && _totalLength == 0) {
            return 0;
        }

        String original = _text;
        int count = 0;

        while (hasMoreLines()) {
            nextLine();
            count++;
        }

        setText(original); // Reset
        return count;
    }

    /**
     * @return True if there are still one or more lines left to paint, false
     *            otherwise.
     */
    public boolean hasMoreLines() {
        return (_position < (_totalLength - 1));
    }

    /**
     * @return The next line to paint.
     */
    public String nextLine() {
        int next = nextPosition();

        if (_start >= _totalLength || next > _totalLength) {
            return null;
        }

        String retval = _text.substring(_start, next);
        _start = next;

        if ((_totalLength - 1 > _start)
                && ((_text.charAt(_start) == LINE_BREAK)
                || (_text.charAt(_start) == WHITE_SPACE)))
        {
            _position++;
            _start++;
        }

        return retval.trim();
    }

    /**
     * @return The next position where to cut the string (i.e. end position of
     *            the next line).
     */
    private int nextPosition() {
        int i = nextWordIndex(_position);
        int lastBreak = -1;
        String line = _text.substring(_position, i);
        int lineWidth = _font.stringWidth(line);

        while (i < _totalLength && lineWidth <= _width) {
            if (_text.charAt(i) == WHITE_SPACE) {
                lastBreak = i;
            }
            else if (_text.charAt(i) == LINE_BREAK) {
                lastBreak = i;
                break;
            }

            if (++i < _totalLength) {
                i = nextWordIndex(i);
                line = _text.substring(_position, i);
                lineWidth = _font.stringWidth(line);
            }
        }

        if (i == _totalLength && lineWidth <= _width) {
            _position = i;
        }
        else if (lastBreak == _position) {
            _position++;
        }
        else if (lastBreak < _position) {
            _position = i;
        }
        else {
            _position = lastBreak;
        }

        return _position;
    }

    /**
     * Resolves the position (index) of the next word from the given
     * position (startIndex).
     * @param startIndex The position where to start looking for the next
     *                      starting word.
     * @return The index of the next starting word.
     */
    private int nextWordIndex(int startIndex) {
        int space = _text.indexOf(WHITE_SPACE, startIndex);
        int newLine = _text.indexOf(LINE_BREAK, startIndex);

        if (space == -1) {
            space = _totalLength;
        }

        if (newLine == -1) {
            newLine = _totalLength;
        }

        if (space < newLine) {
            return space;
        }

        return newLine;
    }
}


code:
/**
 * Copyright (c) 2012-2013 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.lcdui.Font;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

import com.nokia.example.amaze.Main;
import com.nokia.example.amaze.model.MyTimer;

/**
 * A small dialog-like box for displaying tips for the user.
 */
public class TipBox implements MyTimer.Listener {
    // Constants
    private static final int WIDTH = 220;
    private static final int TEXT_WIDTH = 180;
    private static final int TEXT_HEIGHT = 100;
    private static final int TEXT_X_MARGIN = 20;
    private static final int TEXT_Y_TOP_MARGIN = 40;
    private static final int DEFAULT_Y_POSITION = 180;
    private static final int X_POSITION = (240 - WIDTH) / 2;
    private static final int TIMEOUT = 8000; // Milliseconds

    // Members
    static private TipBox _instance = null;
    private Image _backgroundImage = null;
    private TextWrapper _textWrapper = null;
    private String _text = null;
    private boolean _visible = false;
    private int _screenHeight = 0;
    private int _y = 0;
    private int _textY = 0;

    /**
     * Constructor.
     */
    private TipBox(final int screenHeight) {
        _backgroundImage = Main.makeImage("/graphics/tip-box-bg.png");
        _textWrapper = TextWrapper.instance(Font.getDefaultFont(), TEXT_WIDTH);
        _screenHeight = screenHeight;

        if (_backgroundImage == null) {
            _y = DEFAULT_Y_POSITION;
        }
        else {
            _y = (screenHeight - _backgroundImage.getHeight()) / 2;
        }
    }

    /**
     * Shows the tip box.
     * @param text The tip text to show.
     */
    static public void show(final String text, final int screenHeight) {
        if (_instance == null) {
            _instance = new TipBox(screenHeight);
        }

        if (text != null && text.length() > 0) {
            _instance._text = text;
            _instance._visible = true;
            _instance._textWrapper.setText(_instance._text);
            _instance._textY =
                    (TEXT_HEIGHT - _instance._textWrapper.lineCount()
                            * _instance._textWrapper.lineHeight()) / 2;

            // Set the timer to hide the tip box after timeout
            MyTimer.instance().addListener(_instance, false, TIMEOUT);
        }
    }

    /**
     * Hides the tip box.
     */
    static public void hide() {
        // Automatic tip showing ->
        if (tipsToShow > 0) {
            _instance._visible = false;
            _instance._text = null;
            return;
        }
        // <- Automatic tip showing

        if (_instance != null) {
            MyTimer.instance().removeListener(_instance);
            _instance._visible = false;
            _instance._text = null;
            free();
        }
    }

    /**
     * @return True if the tip is visible, false otherwise.
     */
    static public boolean visible() {
        if (_instance != null) {
            return _instance._visible;
        }

        return false;
    }

    /**
     * Releases the handle of the singleton instance so the garbage collector
     * can collect it.
     */
    static public void free() {
        if (_instance != null) {
            System.out.println("TipBox::free(): Releasing the instance.");
            _instance = null;
        }
    }

    // LOGIC FOR AUTOMATICALLY SHOWING THE TIPS STARTS HERE
    // But some of it is added to timeout() and hide()
    static final private int TIP_INTERVAL = TIMEOUT; // Milliseconds
    static final private String[] TIPS =
        {
        /* 3rd */ "Tip: To recalibrate the accelerometer sensor pause and resume the game.",
        /* 2nd */ "Tip: Douple tap the screen to toggle between the set and the default zoom.",
        /* 1st */ "Tip: Pinch the screen to zoom in and out."
        };
    static private int tipsToShow = TIPS.length;

    /**
     * Starts the timed tip showing procedure.
     */
    static public void showTips(final int screenHeight) {
        if (_instance == null) {
            _instance = new TipBox(screenHeight);
        }

        MyTimer.instance().addListener(_instance, true, TIP_INTERVAL);
    }
    // LOGIC FOR AUTOMATICALLY SHOWING THE TIPS STOPS HERE

    /**
     * Static public interface for painting the tip box.
     * @param graphics
     */
    static public void paint(Graphics graphics) {
        if (_instance != null) {
            _instance.doPaint(graphics);
        }
    }

    /**
     * From MyTimer.Listener.
     * Hides the tip box.
     */
    public void onTimeout() {
        System.out.println("TipBox::onTimeout()");

        // Automatic tip showing ->
        if (tipsToShow > 0) {
            if (_text == null) {
                show(TIPS[tipsToShow - 1], _screenHeight);
                tipsToShow--;
            }
            else {
                _visible = false;
                _text = null;
            }

            return;
        }
        // <- Automatic tip showing

        hide();
    }

    /**
     * Paints the tip box.
     * @param graphics
     */
    private void doPaint(Graphics graphics) {
        if (!_visible || _text == null || _text.length() == 0) {
            return;
        }

        // Draw background
        final int topLeft = Graphics.TOP | Graphics.LEFT;
        graphics.drawImage(_backgroundImage, X_POSITION, _y, topLeft);

        // Draw the tip text
        graphics.setColor(MazeCanvas.TEXT_COLOR);
        _textWrapper.setWidth(TEXT_WIDTH);
        int y = _y + TEXT_Y_TOP_MARGIN + _textY;

        try {
            _textWrapper.setText(_text);
            String line = null;

            while (_textWrapper.hasMoreLines()) {
                line = _textWrapper.nextLine();
                graphics.drawString(line,
                        X_POSITION + TEXT_X_MARGIN
                            + (TEXT_WIDTH - _textWrapper.lineWidth(line)) / 2,
                        y, topLeft);
                y += _textWrapper.lineHeight();
            }
        }
        catch (IllegalArgumentException e) {}
    }
}


code:
/**
 * Copyright (c) 2012-2013 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 java.util.Enumeration;
import java.util.Vector;

import javax.microedition.lcdui.Image;
import javax.microedition.m3g.Appearance;
import javax.microedition.m3g.Background;
import javax.microedition.m3g.CompositingMode;
import javax.microedition.m3g.Image2D;
import javax.microedition.m3g.Mesh;
import javax.microedition.m3g.Node;
import javax.microedition.m3g.PolygonMode;
import javax.microedition.m3g.Texture2D;
import javax.microedition.m3g.Transform;
import javax.microedition.m3g.World;

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

/**
 * Helper class containing methods to create the world content. This
 * functionality is separated into its own class so that MazeCanvas
 * wouldn't be so full.
 */
public class WorldBuilder {
    // Constants
    private static final int MAX_NODES_TO_DESTROY_COUNT = 70;

    // Filenames of graphical assets
    private static final String BACKGROUND_IMAGE_FILENAME = "/graphics/background.jpg";
    private static final String MARBLE_IMAGE_FILENAME = "/graphics/marble.png";
    private static final String FLOOR_IMAGE_FILENAME = "/graphics/floor.png";
    private static final String WALL_IMAGE_FILENAME = "/graphics/wall.png";
    private static final String GOAL_IMAGE_FILENAME = "/graphics/goal.png";

    // Members
    private Node[] _nodesToDestroy = null;

    /**
     * Constructor.
     */
    public WorldBuilder() {
        _nodesToDestroy = new Node[MAX_NODES_TO_DESTROY_COUNT];
    }

    /**
     * Sets up the maze.
     * @param world
     * @param maze
     * @param wallAppearance
     * @param wallClearAppearance
     */
    public void createNewMaze(World world,
                              Maze maze,
                              Appearance wallAppearance)
    {
        // Destroy the previous maze if one exists
        destroyMaze(world);

        // Generate a new maze
        maze.createNew(MazeCanvas.MAZE_CORRIDOR_COUNT);

        // Create the planes based on the generated maze
        Enumeration wallsEnum = createPlanes(maze);

        // Release resources not needed anymore
        maze.clear();

        // Create meshes of the planes and add them to the world
        while (wallsEnum.hasMoreElements()) {
            Mesh wallMesh = ((Plane)wallsEnum.nextElement()).createMesh();
            wallMesh.setAppearance(0, wallAppearance);
            world.addChild(wallMesh);
            addNodeToDestroy(wallMesh);
        }

        // Create the goal mark to the end of the maze
        createGoalMark(world, maze);
    }

    /**
     * Creates the background.
     * @param world
     * @return The created background instance.
     */
    public Background createBackground(World world) {
        Background background = new Background();
        Image backgroundImage = Main.makeImage(BACKGROUND_IMAGE_FILENAME);

        if (backgroundImage != null) {
            background.setImage(new Image2D(Image2D.RGB, backgroundImage));
            background.setImageMode(Background.REPEAT, Background.REPEAT);
            world.setBackground(background);
        }

        return background;
    }

    /**
     * Creates the wall appearances.
     * @param wallAppearance
     * @param wallClearAppearance
     */
    public void createWallAppearances(Appearance wallAppearance,
                                      Appearance wallClearAppearance)
    {
        // The walls need perspective correction enabled
        PolygonMode wallPolygonMode = new PolygonMode();
        wallPolygonMode.setPerspectiveCorrectionEnable(true);

        // Build the wall semi-transparent appearance
        wallClearAppearance.setPolygonMode(wallPolygonMode);

        // This is to make a wall semi-transparent
        CompositingMode wallClearCompositeMode = new CompositingMode();
        wallClearCompositeMode.setBlending(CompositingMode.ALPHA_ADD);
        wallClearAppearance.setCompositingMode(wallClearCompositeMode);

        // Build the normal wall appearance
        wallAppearance.setPolygonMode(wallPolygonMode);

        // Load the wall texture
        Image wallTextureImage = Main.makeImage(WALL_IMAGE_FILENAME);

        if (wallTextureImage != null) {
            Texture2D wallTexture = null;
            wallTexture = new Texture2D(new Image2D(Image2D.RGB, wallTextureImage));

            // The texture is repeated
            wallTexture.setWrapping(Texture2D.WRAP_REPEAT,
                                    Texture2D.WRAP_REPEAT);//Texture2D.WRAP_CLAMP);
            wallTexture.setBlending(Texture2D.FUNC_REPLACE);
            wallTexture.setFiltering(Texture2D.FILTER_LINEAR,
                                     Texture2D.FILTER_NEAREST);
            // Set the texture
            wallAppearance.setTexture(0, wallTexture);
            wallClearAppearance.setTexture(0, wallTexture);
        }
    }

    /**
     * Creates the floor.
     * @param world
     */
    public void createFloor(World world) {
        float floorSide = MazeCanvas.MAZE_SIDE_LENGTH / 2;

        // define the location and size of the floor
        Transform floorTransform = new Transform();
        floorTransform.postRotate(90.0f, -1.0f, 0.0f, 0.0f);
        floorTransform.postScale(floorSide, floorSide, 1.0f);

        // The floor appearance. Basically a texture repeated many times
        Appearance floorAppearance = new Appearance();

        // The floor needs that perspective correction is enabled
        PolygonMode floorPolygonMode = new PolygonMode();
        floorPolygonMode.setPerspectiveCorrectionEnable(true);
        floorAppearance.setPolygonMode(floorPolygonMode);

        // Load the texture
        Texture2D floorTexture = null;
        Image floorTextureImage = Main.makeImage(FLOOR_IMAGE_FILENAME);

        if (floorTextureImage != null) {
            floorTexture = new Texture2D(
              new Image2D(Image2D.RGB, floorTextureImage));

            // the texture is repeated many times
            floorTexture.setWrapping(Texture2D.WRAP_REPEAT,
                                     Texture2D.WRAP_REPEAT);
            floorTexture.setBlending(Texture2D.FUNC_REPLACE);
            floorTexture.setFiltering(Texture2D.FILTER_LINEAR,
                                      Texture2D.FILTER_NEAREST);
            floorAppearance.setTexture(0, floorTexture);
        }

        Plane floor = new Plane(floorTransform, 10);

        // Build the mesh
        Mesh floorMesh = floor.createMesh();
        floorMesh.setAppearance(0, floorAppearance);

        // The floor is not pickable
        floorMesh.setPickingEnable(false);

        // Add to the world
        world.addChild(floorMesh);
    }

    /**
     * Creates the goal mark.
     * @param world
     * @param maze
     */
    private void createGoalMark(World world, Maze maze) {
        Appearance appearance = new Appearance();

        CompositingMode compositingMode = new CompositingMode();
        compositingMode.setBlending(CompositingMode.ALPHA);
        appearance.setCompositingMode(compositingMode);

        Texture2D texture = null;
        Image textureImage = Main.makeImage(GOAL_IMAGE_FILENAME);

        if (textureImage != null) {
            texture = new Texture2D(
              new Image2D(Image2D.RGBA, textureImage));

            // The texture is not repeated
            texture.setWrapping(Texture2D.WRAP_CLAMP,
                                Texture2D.WRAP_CLAMP);
            texture.setBlending(Texture2D.FUNC_REPLACE);
            texture.setFiltering(Texture2D.FILTER_NEAREST,
                                 Texture2D.FILTER_NEAREST);
            appearance.setTexture(0, texture);
        }

        // Create the goal mesh
        Plane goalMarkPlane = createGoalMark(maze);
        Mesh goalMarkMesh = goalMarkPlane.createMesh();
        goalMarkMesh.setAppearance(0, appearance);
        goalMarkMesh.setPickingEnable(false); // Not pickable

        world.addChild(goalMarkMesh);
        addNodeToDestroy(goalMarkMesh);
    }

    /**
     * Creates the marble.
     * @param world
     * @return The newly created marble as a Mesh instance.
     */
    public Mesh createMarble(World world) {
        Transform transform = new Transform();
        transform.postRotate(90.0f, -1.0f, 0.0f, 0.0f);
        transform.postScale(MarbleModel.DEFAULT_SIZE,
                            MarbleModel.DEFAULT_SIZE, 1.0f);

        Appearance appearance = new Appearance();
        CompositingMode compositingMode = new CompositingMode();
        compositingMode.setBlending(CompositingMode.ALPHA);
        appearance.setCompositingMode(compositingMode);

        Texture2D texture = null;
        Image marbleImage = Main.makeImage(MARBLE_IMAGE_FILENAME);

        if (marbleImage != null) {
            texture = new Texture2D(new Image2D(Image2D.RGBA, marbleImage));

            // The texture is not repeated
            texture.setWrapping(Texture2D.WRAP_CLAMP,
                                Texture2D.WRAP_CLAMP);
            texture.setBlending(Texture2D.FUNC_REPLACE);
            texture.setFiltering(Texture2D.FILTER_NEAREST,
                                 Texture2D.FILTER_NEAREST);
            appearance.setTexture(0, texture);
        }

        Plane marblePlane = new Plane(transform, 1);
        Mesh marble = marblePlane.createMesh();
        marble.setAppearance(0, appearance);
        marble.setRenderingEnable(false);
        marble.setPickingEnable(false);

        world.addChild(marble);

        return marble;
    }

    /**
     * Destroys the maze planes and goal mark.
     * @param world The world from which to remove the nodes.
     */
    public void destroyMaze(World world) {
        if (_nodesToDestroy != null) {
            for (int i = 0; i < MAX_NODES_TO_DESTROY_COUNT; ++i) {
                if (_nodesToDestroy[i] != null) {
                    world.removeChild(_nodesToDestroy[i]);
                }
            }

            _nodesToDestroy = null;
        }
    }

    /**
     * Adds a reference of the given node to the internal array so that it can
     * be removed from the world instance and deleted later (when a new maze
     * needs to be generated).
     * @param node The node to destroy later.
     */
    private boolean addNodeToDestroy(Node node) {
        if (_nodesToDestroy == null) {
            _nodesToDestroy = new Node[MAX_NODES_TO_DESTROY_COUNT];
            _nodesToDestroy[0] = node;
            return true;
        }

        for (int i = 0; i < MAX_NODES_TO_DESTROY_COUNT; ++i) {
            if (_nodesToDestroy[i] == null) {
                _nodesToDestroy[i] = node;
                return true;
            }
        }

        // The array is full
        System.out.println("WorldBuilder::addNodeToDestroy(): The array is full!");
        return false;
    }

    /**
     * Creates a plane located at the end of the maze.
     * @return The goal plane.
     */
    private Plane createGoalMark(Maze maze) {
        Transform markTransform = new Transform();
        markTransform.postTranslate(maze.origin() + maze.goalX()
                                    * maze.spaceBetweenPlanes(),
                                    maze.height() / 2 + 0.2f,
                                    -maze.origin() - 5f);
        markTransform.postScale(10f, 10f, 10f);
        markTransform.postRotate(90f, -1f, 0f, 0f);
        return new Plane(markTransform, 1f);
    }

    /**
     * Creates the horizontal and vertical planes and puts them in an
     * enumeration. Note that after calling this method the maze array becomes
     * unusable as it is released (set to null).
     * @param maze The model of the maze.
     * @return The enumeration of the components in the planes vector.
     */
    private Enumeration createPlanes(Maze maze) {
        long[] mazeArray = maze.array();

        if (mazeArray == null || mazeArray.length == 0) {
            return null;
        }

        float spaceBetweenPlanes = maze.spaceBetweenPlanes();
        float mazeOrigin = maze.origin();
        float mazeHeight = maze.height();
        Vector allPlanes = new Vector();

        for (int i = 0; i < mazeArray.length; i++) {
            int startX = -1;

            for (int j = 0; j < mazeArray.length; j++) {
                long shift = (0x1L << j);

                if ((mazeArray[i] & shift) == shift && startX == -1) {
                    startX = j;
                    continue;
                }

                if ((((mazeArray[i] & shift) == 0) || (j == (mazeArray.length - 1)))
                        && (startX >= 0))
                {
                    int steps = j - startX;

                    // Don't create walls of side 1 since they will be created
                    // on the other direction
                    if (steps == 1) {
                        startX = -1;
                        continue;
                    }

                    // compensate that the last item is always 1
                    if (j == (mazeArray.length - 1)) {
                        steps++;
                    }

                    Transform planeTransform = new Transform();

                    // Divided by 2 since the original square is of side 2
                    float wallWidth = (maze.spaceBetweenPlanes() * (steps - 1) / 2);

                    // Move to the correct position
                    planeTransform.postTranslate(
                            mazeOrigin + spaceBetweenPlanes * startX + wallWidth,
                            mazeHeight, mazeOrigin + spaceBetweenPlanes * i);

                    // Give the actual size
                    planeTransform.postScale(wallWidth, mazeHeight, 1f);
                    allPlanes.addElement(new Plane(planeTransform, 1f));
                    startX = -1;
                }
            }
        }

        for (int i = 0; i < mazeArray.length; i++) {
            int startY = -1;
            long shift = (0x1L << i);

            for (int j = 0; j < mazeArray.length; j++) {
                if ((mazeArray[j] & shift) == shift && startY == -1) {
                    startY = j;
                    continue;
                }

                if ((((mazeArray[j] & shift) == 0) || (j == (mazeArray.length - 1)))
                        && (startY >= 0))
                {
                    int steps = j - startY;

                    if (steps == 1) {
                        startY = -1;
                        continue;
                    }

                    if (j == (mazeArray.length - 1)) {
                        steps++;
                    }

                    Transform planeTransform = new Transform();

                    // Divided by 2 since the original square is of side 2
                    float wallWidth = (spaceBetweenPlanes * (steps - 1) / 2);

                    // Translate to the correct position
                    planeTransform.postTranslate(
                            mazeOrigin + spaceBetweenPlanes * i, mazeHeight,
                            mazeOrigin + spaceBetweenPlanes * startY + wallWidth);

                    // Rotate 90 degrees since this is a vertical wall
                    planeTransform.postRotate(90f, 0f, 1f, 0f);

                    // Give the correct size
                    planeTransform.postScale(wallWidth, mazeHeight, 1f);
                    allPlanes.addElement(new Plane(planeTransform, 1f));
                    startY = -1;
                }
            }
        }

        return allPlanes.elements();
    }
}


code:
clone_depth: 1
version: "{build}"
image: Visual Studio 2017
platform:
  - x86
  - x64
environment:
  matrix:
    - compiler: msvc
install:
  - cd C:\Tools\vcpkg
  - git pull
  - .\bootstrap-vcpkg.bat
  - cd %APPVEYOR_BUILD_FOLDER%
  - vcpkg install --triplet %PLATFORM%-windows --recurse fftw3 libsamplerate libsndfile lilv lv2 sdl2
  - nuget install clcache -Version 4.1.0
build_script:
  - cd %APPVEYOR_BUILD_FOLDER%
  - mkdir build
  - cd build
  - ps: $env:CMAKE_PLATFORM="$(if ($env:PLATFORM -eq 'x64') { 'x64' } else { '' })"
  - ps: $env:QT_SUFFIX="$(if ($env:PLATFORM -eq 'x64') { '_64' } else { '' })"
  - cmake -DUSE_COMPILE_CACHE=ON -DCACHE_TOOL=%APPVEYOR_BUILD_FOLDER%/clcache.4.1.0/clcache-4.1.0/clcache.exe -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_PREFIX_PATH=c:/Qt/5.12/msvc2017%QT_SUFFIX%;c:/tools/vcpkg/installed/%PLATFORM%-windows -DCMAKE_GENERATOR_PLATFORM="%CMAKE_PLATFORM%" ..
  - cmake --build . -- /maxcpucount:4
  - cmake --build . --target tests
  - cmake --build . --target package
artifacts:
  - path: 'build\lmms-*.exe'
    name: Installer
cache:
  - c:/tools/vcpkg/installed

repo_name: CYBERDEViL/LMMS

license: mit


code:
version: 2

shared:
  restore_cache: &restore_cache
    restore_cache:
      keys:
        - ccache-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}
        - ccache-{{ arch }}-{{ .Environment.CIRCLE_JOB }}
        - ccache-{{ arch }}
  save_cache: &save_cache
    save_cache:
      key: ccache-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ .BuildNum }}
      paths:
        - ~/.ccache
  restore_homebrew_cache: &restore_homebrew_cache
    restore_cache:
      keys:
        - homebrew-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}
        - homebrew-{{ arch }}-{{ .Environment.CIRCLE_JOB }}
        - homebrew-{{ arch }}
  save_homebrew_cache: &save_homebrew_cache
    save_cache:
      key: homebrew-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ .BuildNum }}
      paths:
        - ~/Library/Caches/Homebrew
        - /usr/local/Homebrew

  ccache_stats: &ccache_stats
    run:
      name: Print ccache statistics
      command: |
        echo "[ccache config]"
        ccache -p
        echo "[ccache stats]"
        ccache -s

  # Commmon initializing commands
  init: &init
    run:
      name: Initialize
      command: |
        mkdir -p /tmp/artifacts
        # Workaround for failing submodule fetching
        git config --global --unset url."ssh://git@github.com".insteadOf || true
        if [[ -n "${CIRCLE_PR_NUMBER}" ]]
        then
            echo "Fetching out merged pull request"
            git fetch -u origin refs/pull/${CIRCLE_PR_NUMBER}/merge:pr/merge
            git checkout pr/merge
        else
            echo "Not a pull request"
        fi

  # Commmon environment variables
  common_environment: &common_environment
    QT5: True
    CMAKE_OPTS: -DUSE_WERROR=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUSE_CCACHE=ON
    CCACHE_MAXSIZE: 500M
    CCACHE_LOGFILE: /tmp/artifacts/ccache.log
    MAKEFLAGS: -j6

jobs:
  mingw32:
    environment:
      <<: *common_environment
    docker:
      - image: lmmsci/linux.mingw32:18.04
    steps:
      - checkout
      - *init
      - *restore_cache
      - run:
          name: Building
          command: |
            mkdir build && cd build
            ../cmake/build_win32.sh
            make lmms
            make
      - run:
          name: Build tests
          command: cd build && make tests
      - run:
          name: Build installer
          command: |
            cd build
            make package
            cp ./lmms-*.exe /tmp/artifacts/
      - store_artifacts:
          path: /tmp/artifacts/
          destination: /
      - *ccache_stats
      - *save_cache
  mingw64:
    environment:
      <<: *common_environment
    docker:
      - image: lmmsci/linux.mingw64:18.04
    steps:
      - checkout
      - *init
      - *restore_cache
      - run:
          name: Building
          command: |
            mkdir build && cd build
            ../cmake/build_win64.sh
            make
      - run:
          name: Build tests
          command: cd build && make tests
      - run:
          name: Build installer
          command: |
            cd build
            make package
            cp ./lmms-*.exe /tmp/artifacts/
      - store_artifacts:
          path: /tmp/artifacts/
          destination: /
      - *ccache_stats
      - *save_cache
  linux.gcc:
    docker:
      - image: lmmsci/linux.gcc:16.04
    environment:
      <<: *common_environment
    steps:
      - checkout
      - *init
      - *restore_cache
      - run:
          name: Configure
          command: |
            source /opt/qt5*/bin/qt5*-env.sh || true
            mkdir build && cd build
            cmake .. $CMAKE_OPTS -DCMAKE_INSTALL_PREFIX=./install
      - run:
          name: Build
          command: cd build && make
      - run:
          name: Build tests
          command: cd build && make tests
      - run:
          name: Run tests
          command: build/tests/tests
      - *ccache_stats
      - run:
          name: Build AppImage
          command: |
            cd build
            make install
            make appimage || (cat appimage.log && false)
            cp ./lmms-*.AppImage /tmp/artifacts/
      - store_artifacts:
          path: /tmp/artifacts/
          destination: /
      - store_artifacts:
          path: build/appimage.log
          destination: /
      - *save_cache
  shellcheck:
    docker:
      - image: koalaman/shellcheck-alpine:v0.4.6
    steps:
      - checkout
      - run:
          name: Shellcheck
          command: shellcheck $(find "./cmake/" -type f -name '*.sh' -o -name "*.sh.in")
  macos:
    environment:
      <<: *common_environment
    macos:
      xcode: "10.3.0"
    steps:
      - checkout
      - *init
      - *restore_homebrew_cache
      - *restore_cache
      - run:
          name: Install Homebrew dependencies
          command: |
            # uninstall Homebrew's python 2 to prevent errors on brew install
            brew uninstall python@2 || true
            # Working around for https://github.com/Homebrew/brew/pull/9383
            (git -C "/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core" fetch && git merge FETCH_HEAD --ff-only) || true
            (git -C "/usr/local/Homebrew" fetch && git merge FETCH_HEAD --ff-only) || true
            brew install ccache fftw cmake pkg-config libogg libvorbis lame libsndfile libsamplerate jack sdl libgig libsoundio lilv lv2 stk fluid-synth portaudio fltk qt5 carla
      - run:
          name: Install nodejs dependencies
          command: npm install -g appdmg
      - run:
          name: Building
          command: |
            mkdir build && cd build
            cmake .. -DCMAKE_INSTALL_PREFIX="../target" -DCMAKE_PREFIX_PATH="$(brew --prefix qt5)" $CMAKE_OPTS -DUSE_WERROR=OFF
            make
      - run:
          name: Build tests
          command: cd build && make tests
      - run:
          name: Run tests
          command: build/tests/tests
      - run:
          name: Build DMG
          command: |
            cd build
            make install
            make dmg
            cp ./lmms-*.dmg /tmp/artifacts/
      - store_artifacts:
          path: /tmp/artifacts/
          destination: /
      - *save_cache
      - *save_homebrew_cache

workflows:
  version: 2
  build-and-test:
    jobs:
      - macos
      - mingw32
      - mingw64
      - linux.gcc
      - shellcheck


code:
root = true

[*]
indent_style = tab
tab_width = 4

path: .editorconfig


code:
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

# Please search the issue tracker for existing bug reports before submitting your own. Delete this line to confirm no similar report has been posted yet.

### Bug Summary

#### Steps to reproduce

#### Expected behavior

#### Actual behavior

#### Screenshot

#### Affected LMMS versions

#### Logs

  Click to expand


code:
contact_links:
- name: Get help on Discord
  url: https://lmms.io/chat/
  about: Need help? Have a question? Reach out to other LMMS users on our Discord server!


code:
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---

# Please search the issue tracker for existing feature requests before submitting your own. Delete this line to confirm no similar request has been posted yet.

### Enhancement Summary

#### Justification

#### Mockup

<!-- If your request encompasses changes to the user interface, provide a mockup of your proposal here -->


code:
# Label requiring a response
responseRequiredLabel: "response required"


code:
[submodule "src/3rdparty/qt5-x11embed"]
	path = src/3rdparty/qt5-x11embed
	url = https://github.com/Lukas-W/qt5-x11embed.git
[submodule "src/3rdparty/rpmalloc/rpmalloc"]
	path = src/3rdparty/rpmalloc/rpmalloc
	url = https://github.com/mjansson/rpmalloc.git
[submodule "plugins/zynaddsubfx/zynaddsubfx"]
	path = plugins/zynaddsubfx/zynaddsubfx
	url = https://github.com/lmms/zynaddsubfx.git
[submodule "plugins/FreeBoy/game-music-emu"]
	path = plugins/FreeBoy/game-music-emu
	url = https://bitbucket.org/mpyne/game-music-emu.git
[submodule "plugins/OpulenZ/adplug"]
	path = plugins/OpulenZ/adplug
	url = https://github.com/adplug/adplug.git
[submodule "plugins/LadspaEffect/calf/veal"]
	path = plugins/LadspaEffect/calf/veal
	url = https://github.com/lmms/veal
[submodule "plugins/Xpressive/exprtk"]
	path = plugins/Xpressive/exprtk
	url = https://github.com/ArashPartow/exprtk
[submodule "plugins/LadspaEffect/swh/ladspa"]
	path = plugins/LadspaEffect/swh/ladspa
	url = https://github.com/swh/ladspa
[submodule "plugins/LadspaEffect/tap/tap-plugins"]
	path = plugins/LadspaEffect/tap/tap-plugins
	url = https://github.com/tomszilagyi/tap-plugins
[submodule "src/3rdparty/weakjack/weakjack"]
	path = src/3rdparty/weakjack/weakjack
	url = https://github.com/x42/weakjack.git
[submodule "src/3rdparty/mingw-std-threads"]
	path = src/3rdparty/mingw-std-threads
	url = https://github.com/meganz/mingw-std-threads.git
[submodule "doc/wiki"]
	path = doc/wiki
	url = https://github.com/lmms/lmms.wiki.git
[submodule "src/3rdparty/ringbuffer"]
	path = src/3rdparty/ringbuffer
	url = https://github.com/JohannesLorenz/ringbuffer.git
[submodule "plugins/carlabase/carla"]
	path = plugins/carlabase/carla
	url = https://github.com/falktx/carla
[submodule "plugins/sid/resid"]
	path = plugins/Sid/resid
	url = https://github.com/simonowen/resid
[submodule "src/3rdparty/jack2"]
	path = src/3rdparty/jack2
	url = https://github.com/jackaudio/jack2
[submodule "plugins/LadspaEffect/cmt/cmt"]
	path = plugins/LadspaEffect/cmt/cmt
	url = https://github.com/lmms/cmt

path: .gitmodules