Alvaro2112/Money_run

View on GitHub
app/src/main/java/sdp/moneyrun/ui/map/MapActivity.java

Summary

Maintainability
D
2 days
Test Coverage
A
90%
package sdp.moneyrun.ui.map;

import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.Chronometer;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.collection.LongSparseArray;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.GenericTypeIndicator;
import com.google.firebase.database.ValueEventListener;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.offline.OfflineManager;
import com.mapbox.mapboxsdk.offline.OfflineRegion;
import com.mapbox.mapboxsdk.plugins.annotation.CircleManager;
import com.mapbox.mapboxsdk.plugins.annotation.CircleOptions;
import com.mapbox.mapboxsdk.plugins.annotation.Symbol;
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager;
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions;
import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions;
import com.mapbox.mapboxsdk.utils.BitmapUtils;
import com.mapbox.mapboxsdk.utils.ColorUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import sdp.moneyrun.Helpers;
import sdp.moneyrun.R;
import sdp.moneyrun.database.DatabaseProxy;
import sdp.moneyrun.database.game.GameDatabaseProxy;
import sdp.moneyrun.database.riddle.Riddle;
import sdp.moneyrun.database.riddle.RiddlesDatabase;
import sdp.moneyrun.game.Game;
import sdp.moneyrun.map.Coin;
import sdp.moneyrun.map.CoinGenerationHelper;
import sdp.moneyrun.map.LocationCheckObjectivesCallback;
import sdp.moneyrun.map.MapPlayerListAdapter;
import sdp.moneyrun.map.TrackedMap;
import sdp.moneyrun.player.LocalPlayer;
import sdp.moneyrun.player.Player;


/*
this map implements all the functionality we will need.
 */
@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal"})
public class MapActivity extends TrackedMap implements OnMapReadyCallback {
    public static final int THRESHOLD_DISTANCE = 5;
    public static final float DISTANCE_CHANGE_BEFORE_UPDATE = (float) 2;
    private static final long MINIMUM_TIME_BEFORE_UPDATE = 2000;
    private static int chronometerCounter;
    private final String TAG = MapActivity.class.getSimpleName();
    private final String COIN_ID = "COIN";
    private final float ICON_SIZE = 1.5f;
    private final int DISTANCE_BETWEEN_COINS = 9;
    private final int COIN_PLACEMENT_ATTEMPT_LIMIT = 50;
    private final double scalingFactor = 5000.0;
    private final int MapboxScale = 8;
    private final int numberOfSecondsInAMinute = 60;
    public int coinsToPlace;
    private Chronometer chronometer;
    @Nullable
    private RiddlesDatabase riddleDb;
    private Location currentLocation;
    private Player player;
    private GameDatabaseProxy proxyG;
    private TextView currentScoreView;
    private Button exitButton;
    private Button leaderboardButton;
    private LocalPlayer localPlayer;
    @Nullable
    private Game game;
    private String gameId;
    private boolean addedCoins = false;
    private boolean host = false;
    private MapPlayerListAdapter ldbListAdapter;
    private boolean hasEnded;
    private CircleManager circleManager;
    private double game_radius;
    private int game_time;
    @Nullable
    private Location game_center;
    private float circleRadius;
    private double shrinkingFactor = 0.99;
    private ArrayList<Coin> seenCoins;
    private ValueEventListener isEndedListener;
    private String locationMode;
    private boolean isAnswering;
    private boolean hasFoundMap;
    private OfflineManager offlineManager;
    private boolean startLocationIsSet = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Objects.requireNonNull(getSupportActionBar()).hide();

        getExtras();
        initializeVariables();

        Mapbox.getInstance(this, getString(R.string.mapbox_access_token));
        createMap(savedInstanceState, R.id.mapView, R.layout.activity_map);
        mapView.getMapAsync(this);

        getViews();

        String default_score = getString(R.string.map_score_text, 0);
        currentScoreView.setText(default_score);

        addExitButton();
        addLeaderboardButton();

        mapView.addOnDidFinishRenderingMapListener(fully -> {
            if (gameId != null)
                initializeGame(gameId);
        });

        DatabaseProxy.addOfflineListener(this, TAG);

        if (locationMode != null)
            initLocationManager(locationMode);
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        DatabaseProxy.removeOfflineListener();
    }

    /**
     * Initializes all the logic to keep the location updated
     *
     * @param locationMode the location mode that will be used ("gps" for gps location, "network" for network location and null for the default MapBox location (i.e. getLastLocation))
     */
    public void initLocationManager(String locationMode) {
        LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            return;
        }

        @NonNull
        LocationListener locationListenerGPS = new LocationListener() {
            @Override
            public void onLocationChanged(@NonNull Location location) {
                if (getMapboxMap() != null) {
                    getMapboxMap().getLocationComponent().forceLocationUpdate(location);
                    checkObjectives(location);
                }
            }

            @Override
            public void onProviderDisabled(@NonNull String provider) {

            }

            @Override
            public void onProviderEnabled(@NonNull String provider) {

            }

        };
        locationManager.requestLocationUpdates(locationMode, MINIMUM_TIME_BEFORE_UPDATE, DISTANCE_CHANGE_BEFORE_UPDATE, locationListenerGPS);


    }


    /**
     * get all the extras necessary to run this activity
     */
    public void getExtras() {
        player = (Player) getIntent().getSerializableExtra("player");
        gameId = getIntent().getStringExtra("currentGameId");
        if (gameId == null) {
            gameId = getIntent().getStringExtra("gameId");
        }
        host = getIntent().getBooleanExtra("host", false);
        locationMode = getIntent().getStringExtra("locationMode");

    }

    /**
     * Initialize all the variables to start this activity
     */
    public void initializeVariables() {
        seenCoins = new ArrayList<>();
        proxyG = new GameDatabaseProxy();
        localPlayer = new LocalPlayer();
        hasEnded = false;
        isAnswering = false;
        hasFoundMap = false;
        try {
            riddleDb = RiddlesDatabase.createInstance(getApplicationContext());
        } catch (RuntimeException e) {
            riddleDb = RiddlesDatabase.getInstance();
        }
    }

    /**
     * Find all the views present in this activity
     */
    public void getViews() {
        currentScoreView = findViewById(R.id.map_score_view);
        chronometer = findViewById(R.id.mapChronometer);
        exitButton = findViewById(R.id.close_map);
        leaderboardButton = findViewById(R.id.in_game_scores_button);
    }

    /**
     * @param gameId The game ID to fetch the game from the DB
     *               Place the coins if user is host and coins have not been placed yet
     *               It then finds the game and put the coins in it, the database is then updated
     *               finishes by adding a listener for the coins
     */
    public void initializeGame(String gameId) {
        proxyG.getGameDataSnapshot(gameId).addOnCompleteListener(task -> {
            if (task.isSuccessful()) {
                game = proxyG.getGameFromTaskSnapshot(task);
                coinsToPlace = game.getNumCoins();
                game_radius = game.getRadius();
                circleRadius = (float) game_radius * MapboxScale;
                game_time = (int) Math.floor(game.getDuration() * numberOfSecondsInAMinute);
                setGameCenter();
                initChronometer();

                if (!addedCoins && host) {
                    placeRandomCoins(coinsToPlace, game_radius, THRESHOLD_DISTANCE);
                    addedCoins = true;
                }
                addCoinsListener();
                if (host) {
                    game.setCoins(localPlayer.getLocallyAvailableCoins(), true);
                    proxyG.updateGameInDatabase(game, null);
                } else {
                    localPlayer.setLocallyAvailableCoins((ArrayList<Coin>) game.getCoins());
                }
                initCircleAndSetEndListener();
            } else {
                Log.e(TAG, task.getException().getMessage());
            }
        });
    }

    /**
     * Setting Game Center depending on the host
     */
    private void setGameCenter() {
        game_center = getCurrentLocation();
        if (host)
            game.setStartLocation(game_center, false);
    }

    /**
     * Initializes circle and end game listener
     */
    private void initCircleAndSetEndListener() {
        initCircle();
        if (!host)
            listenEnded();
    }

    /**
     * Adds a coin listener to database proxy,
     * used to update the coin in the current game for the player map
     */
    private void addCoinsListener() {

        proxyG.addCoinListener(game, new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                GenericTypeIndicator<List<Coin>> t = new GenericTypeIndicator<List<Coin>>() {
                };

                List<Coin> newCoins = snapshot.getValue(t);

                if (newCoins == null) {
                    return;
                }

                localPlayer.syncAvailableCoinsFromDb(new ArrayList<>(newCoins));

                symbolManager.deleteAll();

                for (Coin coin : localPlayer.getLocallyAvailableCoins()) {
                    addCoin(coin, false);
                }
                if (!host && !startLocationIsSet) {
                    game_center = game.getStartLocation();
                    startLocationIsSet = true;
                }
            }

            @Override
            public void onCancelled(@NonNull DatabaseError error) {
                Log.e(Game.class.getSimpleName(), error.getMessage());
            }

        });

    }


    /**
     * Adds functionality to the leaderboard button
     */
    private void addLeaderboardButton() {
        leaderboardButton.setOnClickListener(v -> onButtonShowLeaderboard(mapView, true, R.layout.in_game_scores));
    }


    /**
     * Add the functionality of leaving the map Activity
     */
    private void addExitButton() {
        exitButton.setOnClickListener(v -> {
                    if (!hasEnded) {
                        endGame();
                    }
                }
        );
    }

    /**
     * Add functionality to the question button so that we can see random questions popup
     *
     * @param mapboxMap the map where everything will be done
     *                  this overrides the OnMapReadyCallback in the implemented interface
     *                  We set up the symbol manager here, it will allow us to add markers and other visual stuff on the map
     *                  Then we setup the location tracking
     */
    @Override
    public void onMapReady(@NonNull final MapboxMap mapboxMap) {

        callback = new LocationCheckObjectivesCallback(this, locationMode == null);

        mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
            offlineManager = OfflineManager.getInstance(MapActivity.this);
            getDownloadedRegion();

            GeoJsonOptions geoJsonOptions = new GeoJsonOptions().withTolerance(0.4f);
            symbolManager = new SymbolManager(mapView, mapboxMap, style, null, geoJsonOptions);
            symbolManager.setIconAllowOverlap(true);
            symbolManager.setTextAllowOverlap(true);
            style.addImage(COIN_ID, BitmapUtils.getBitmapFromDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.coin_image)), false);
            enableLocationComponent(style);

            circleManager = new CircleManager(mapView, mapboxMap, style);

        });

        this.mapboxMap = mapboxMap;
    }

    public Chronometer getChronometer() {
        return chronometer;
    }

    public int getChronometerCounter() {
        return chronometerCounter;
    }

    public Location getCurrentLocation() {
        return currentLocation;
    }

    public LocalPlayer getLocalPlayer() {
        return localPlayer;
    }

    public double getGameRadius() {
        return game_radius;
    }

    public double getGameDuration() {
        return game_time;
    }

    @Nullable
    public Location getGameCenter() {
        return game_center;
    }

    public boolean getHasFoundMap() {
        return hasFoundMap;
    }


    @Nullable
    public String getPlayerId() {
        return player.getPlayerId();
    }

    public MapPlayerListAdapter getLdbListAdapter() {
        return ldbListAdapter;
    }

    /**
     * The chronometer will countdown from the maximum time of a game to 0
     */
    private void initChronometer() {

        setupChronometer();
        chronometer.setOnChronometerTickListener(chronometer -> {
            if (chronometerCounter < game_time) {
                chronometerCounter += 1;
                circleRadius -= shrinkingFactor;
                if (chronometerCounter % 2 == 0)
                    initCircle();
            } else {
                if (!hasEnded) {
                    if (host)
                        game.setEnded(true, false);
                    endGame();
                }
            }
            chronometer.setFormat("REMAINING TIME " + (game_time - chronometerCounter));
        });
    }

    /**
     * Helper method to set the chronometer functionality
     */
    private void setupChronometer() {
        chronometer.start();
        chronometerCounter = 0;
        chronometer.setFormat("REMAINING TIME " + (game_time - chronometerCounter));
        shrinkingFactor = (circleRadius) / (2 * game_time);
        long current = System.currentTimeMillis() / 1000;
        if (host) {
            game.setStartTime(current, false);
        } else {
            long start = game.getStartTime();
            int loadingTimeInSeconds = 6;
            chronometerCounter = (int) (current - start + loadingTimeInSeconds);
        }
    }


    public void endGame() {
        hasEnded = true;
        Game.endGame(localPlayer.getCollectedCoins().size(), localPlayer.getScore(), player.getPlayerId(), game.getPlayers(), MapActivity.this, false);
    }


    /**
     * Creates the popup where the question and the possible answers will be shown to the user
     *
     * @param view      The view on top of which the popup will be shown
     * @param focusable whether the popup is focused
     * @param layoutId  the layoutId of the popup
     * @param riddle    The riddle that will be asked
     * @param coin      the coin that triggered a riddle
     */
    public void onButtonShowQuestionPopupWindowClick(View view, Boolean focusable, int layoutId, @NonNull Riddle riddle, @Nullable Coin coin) {

        PopupWindow popupWindow = Helpers.onButtonShowPopupWindowClick(this, view, focusable, layoutId);
        popupWindow.setOnDismissListener(() -> {
            if (coin != null) {
                removeCoin(coin, false);
            }
        });
        TextView tv = popupWindow.getContentView().findViewById(R.id.question);
        int correctId;

        //changes the text to the current question
        tv.setText(riddle.getQuestion());

        int[] buttonIds = {R.id.question_choice_1, R.id.question_choice_2, R.id.question_choice_3, R.id.question_choice_4};
        TextView buttonView;

        //Loops to find the ID of the button solution and assigns the text to each button
        for (int i = 0; i < 4; i++) {

            buttonView = popupWindow.getContentView().findViewById(buttonIds[i]);
            buttonView.setText(riddle.getPossibleAnswers()[i]);

            //Found the id of the solution button
            if (riddle.getPossibleAnswers()[i].equals(riddle.getAnswer())) {
                correctId = buttonIds[i];
                correctAnswerListener(popupWindow, correctId, coin, riddle);
            } else {
                wrongAnswerListener(popupWindow, buttonIds[i], coin, riddle);
            }
        }

    }

    /**
     * Function used when clicking the leaderboard button,
     * fetches the game from the database and shows the name and score of the player in descending order
     *
     * @param view      The view on top of which the popup will be shown
     * @param focusable whether the popup is focused
     * @param layoutId  the layoutId of the popup
     */
    @RequiresApi(api = Build.VERSION_CODES.N)
    public void onButtonShowLeaderboard(View view, Boolean focusable, int layoutId) {
        PopupWindow popupWindow = Helpers.onButtonShowPopupWindowClick(this, view, focusable, layoutId);

        proxyG.getGameDataSnapshot(gameId).addOnCompleteListener(task -> {
            if (task.isSuccessful()) {
                game = proxyG.getGameFromTaskSnapshot(task);
                ArrayList<Player> playerList = new ArrayList<>(game.getPlayers());
                playerList.sort((o1, o2) -> Integer.compare(o2.getScore(), o1.getScore()));

                ldbListAdapter = new MapPlayerListAdapter(this, new ArrayList<>());
                ListView leaderboard = popupWindow.getContentView().findViewById(R.id.in_game_scores_listview);
                ldbListAdapter.addAll(playerList);
                leaderboard.setAdapter(ldbListAdapter);
            } else {
                Log.e(TAG, task.getException().getMessage());
            }
        });

    }

    public void closePopupListener(@NonNull PopupWindow popupWindow, int Id) {
        popupWindow.getContentView().findViewById(Id).setOnClickListener(v -> popupWindow.dismiss());
    }

    /**
     * Listener on the wrong possible answers of the riddle
     *
     * @param popupWindow the current popup where the question is displayed
     * @param btnId       the id of one of the incorrect answer buttons
     * @param coin        the coin that triggered the question
     * @param riddle      the riddle that is being displayed
     */
    public void wrongAnswerListener(@NonNull PopupWindow popupWindow, int btnId, @Nullable Coin coin, @NonNull Riddle riddle) {

        popupWindow.getContentView().findViewById(btnId).setOnClickListener(v -> {
            MediaPlayer.create(this, R.raw.wrong_choice).start();
            popupWindow.dismiss();
            PopupWindow wrongAnswerPopupWindow = Helpers.onButtonShowPopupWindowClick(MapActivity.this, mapView, true, R.layout.wrong_answer_popup);
            TextView tv = wrongAnswerPopupWindow.getContentView().findViewById(R.id.question);
            tv.setText(riddle.getQuestion());

            tv = wrongAnswerPopupWindow.getContentView().findViewById(R.id.incorrect_answer_text);
            tv.setText(String.format("%s: '%s'", getString(R.string.incorrect_answer_message), riddle.getAnswer()));
            tv.setTextColor(Color.RED);


            closePopupListener(wrongAnswerPopupWindow, R.id.continue_run);

            if (coin != null) {
                removeCoin(coin, false);
            }
        });
    }


    /**
     * Listener on the correct answer of the riddle
     *
     * @param popupWindow the current popup where the question is displayed
     * @param btnId       the id of the correct answer button
     * @param coin        the coin that triggered the question
     * @param riddle      the riddle that is being displayed
     */
    public void correctAnswerListener(@NonNull PopupWindow popupWindow, int btnId, @Nullable Coin coin, @NonNull Riddle riddle) {

        popupWindow.getContentView().findViewById(btnId).setOnClickListener(v -> {
            MediaPlayer.create(this, R.raw.correct_choice).start();
            popupWindow.dismiss();
            PopupWindow correctAnswerPopupWindow = Helpers.onButtonShowPopupWindowClick(MapActivity.this, mapView, true, R.layout.correct_answer_popup);
            TextView tv = correctAnswerPopupWindow.getContentView().findViewById(R.id.question);
            tv.setText(riddle.getQuestion());

            tv = correctAnswerPopupWindow.getContentView().findViewById(R.id.incorrect_answer_text);
            tv.setTextColor(Color.GREEN);

            closePopupListener(correctAnswerPopupWindow, R.id.collect_coin);
            if (coin != null)
                removeCoin(coin, true);
        });
    }


    /**
     * @param coin Adds a coins to the list of remaining coins and adds it to the map
     */
    public void addCoin(@Nullable Coin coin, boolean addLocal) {
        if (coin == null) {
            throw new NullPointerException("added coin cannot be null");
        }

        if (addLocal) {
            localPlayer.addLocallyAvailableCoin(coin);
        }
        symbolManager.create(new SymbolOptions().withLatLng(new LatLng(coin.getLatitude(), coin.getLongitude())).withIconImage(COIN_ID).withIconSize(ICON_SIZE));
    }


    /**
     * @param coin removes a coin from the list of remaining coins, adds it to the list of collected coin
     *             and removes it from the map
     */
    public void removeCoin(@Nullable Coin coin, Boolean collected) {
        if (coin == null) {
            throw new NullPointerException("removed coined is null");
        }

        localPlayer.updateCoins(coin, collected);

        if (collected) {
            String default_score = getString(R.string.map_score_text, localPlayer.getScore());
            currentScoreView.setText(default_score);
            List<Player> temp_players = game.getPlayers();
            temp_players.remove(player);
            player.setScore(localPlayer.getScore(), true);
            temp_players.add(player);
            game.setPlayers(temp_players, true);
            game.setCoins(localPlayer.toSendToDb(), true);
            proxyG.updateGameInDatabase(game, null);
        }

        deleteCoinFromMap(coin);
        boolean check = checkIfLegalPosition(coin, circleRadius, game_center.getLatitude(), game_center.getLongitude());
        if (check)
            Game.endGame(localPlayer.getCollectedCoins().size(), localPlayer.getScore(), player.getPlayerId(), game.getPlayers(), MapActivity.this, true);

    }

    /**
     * Removes the coin from the symbol manager
     *
     * @param coin Coin to delete from the Symbolmanager
     */
    public void deleteCoinFromMap(@NonNull Coin coin) {
        LongSparseArray<Symbol> symbols = symbolManager.getAnnotations();

        for (int i = 0; i < symbols.size(); ++i) {
            Symbol symbol = symbols.valueAt(i);
            if (coin.getLatitude() == symbol.getLatLng().getLatitude() && coin.getLongitude() == symbol.getLatLng().getLongitude()) {
                symbolManager.delete(symbol);
            }
        }
    }

    /**
     * @param number    number of coins to add
     * @param maxRadius max distance of a coin from the center
     * @param minRadius min distance of a coin from the center
     */
    public void placeRandomCoins(int number, double maxRadius, double minRadius) {
        if (number < 0 || maxRadius <= 0 || minRadius <= 0)
            throw new IllegalArgumentException("Number of coins to place is less than 0, number of coin is  " + number);
        if (minRadius >= maxRadius)
            throw new IllegalArgumentException("Min radius cannot be bigger or equal than max Radius ");
        int addedCoins = 0;
        for (int i = 0; i < number; i++) {
            Location loc = generateSingleLocation(maxRadius, minRadius);
            if (loc != null) {
                addCoin(new Coin(loc.getLatitude(), loc.getLongitude(), CoinGenerationHelper.coinValue(loc, getCurrentLocation())), true);
                addedCoins++;
            }
        }
        if (addedCoins < number) {
            Toast.makeText(this, "Only " + addedCoins + " could be added within the specified radius", Toast.LENGTH_LONG).show();
        }
    }

    @Nullable
    private Location generateSingleLocation(double maxRadius, double minRadius) {
        List<Coin> coins = localPlayer.getLocallyAvailableCoins();
        Location loc;
        int tries = 0;
        do {
            loc = CoinGenerationHelper.getRandomLocation(getCurrentLocation(), maxRadius, minRadius);
            tries++;
        } while (CoinGenerationHelper.minDistWithExistingCoins(loc, coins) < DISTANCE_BETWEEN_COINS && tries < COIN_PLACEMENT_ATTEMPT_LIMIT);
        if (tries == COIN_PLACEMENT_ATTEMPT_LIMIT) {
            loc = null;
        } // if no successful loc was found we return null
        return loc;
    }

    /**
     * @param location Used to check if location is near a coin or not
     */
    @Override
    public void checkObjectives(@NonNull Location location) {
        currentLocation = location;
        Coin coin = nearestCoin(location, localPlayer.getLocallyAvailableCoins(), THRESHOLD_DISTANCE);

        if (coin != null && !seenCoins.contains(coin) && !isAnswering) {
            isAnswering = true;
            seenCoins.add(coin);
            try {
                onButtonShowQuestionPopupWindowClick(mapView, true, R.layout.question_popup, riddleDb.getRandomRiddle(), coin);
            } catch (WindowManager.BadTokenException e) {
                seenCoins.remove(coin);
            }
            isAnswering = false;
        }
    }

    /**
     * Draws a circle of a predefined radius that corresponds to the radius inside which coins get generated
     * and it shrinks by a shrinking factor that is also predefined
     */
    public void initCircle() {
        circleManager.deleteAll();
        circleHelper(game_center, circleRadius / scalingFactor);
    }


    private void circleHelper(@NonNull Location centerCoordinates, double radiusInKilometers) {
        int numberOfSides = 256;
        int halfCircleDegrees = 180;
        double scaleLongitudeToKilometers = 111.319;
        double scaleLatitudeToKilometers = 110.574;
        CircleOptions circleOptions = new CircleOptions();
        double distanceX = radiusInKilometers / (scaleLongitudeToKilometers * Math.cos(centerCoordinates.getLatitude() * Math.PI / halfCircleDegrees));
        double distanceY = radiusInKilometers / scaleLatitudeToKilometers;
        double slice = (2 * Math.PI) / numberOfSides;
        double theta;
        double x;
        double y;
        LatLng position;
        for (int i = 0; i < numberOfSides; ++i) {
            theta = i * slice;
            x = distanceX * Math.cos(theta);
            y = distanceY * Math.sin(theta);
            position = new LatLng(centerCoordinates.getLatitude() + y,
                    centerCoordinates.getLongitude() + x);
            circleManager.create(circleOptions.withCircleRadius(3f).withLatLng(position).withCircleColor(ColorUtils.colorToRgbaString(getResources().getColor(R.color.colorPrimary))));
        }
    }


    /**
     * Check if there is a downloaded map and show a toast if there is one or not
     */
    private void getDownloadedRegion() {
        offlineManager.listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() {

            @Override
            public void onList(@Nullable OfflineRegion[] offlineRegions) {
                if (offlineRegions == null || offlineRegions.length == 0) {
                    hasFoundMap = false;
                    Toast.makeText(getApplicationContext(), getString(R.string.no_offline_regions), Toast.LENGTH_SHORT).show();
                    return;
                }
                Toast.makeText(getApplicationContext(), getString(R.string.found_offline_regions), Toast.LENGTH_SHORT).show();

                hasFoundMap = true;
            }

            @Override
            public void onError(String error) {
                Toast.makeText(MapActivity.this, getString(R.string.no_offline_regions), Toast.LENGTH_SHORT).show();
            }
        });
    }


    public CircleManager getCircleManager() {
        return circleManager;
    }

    /**
     * Checks if the coin the player wants to pick up is allowed
     *
     * @param coin     coin that the player wants to pick up
     * @param radius   the current radius of the game
     * @param center_x the x coordinate of the center of the game
     * @param center_y the y coordinate of the center of the game
     * @return true if the coin is inside the game circle
     */
    public boolean checkIfLegalPosition(@NonNull Coin coin, double radius, double center_x, double center_y) {
        // calculates distance between the current coin and the game center
        double distance = Math.sqrt(Math.pow(coin.getLatitude() - center_x, 2) + Math.pow(coin.getLongitude() - center_y, 2));
        return (distance > radius);
    }


    /**
     * Sets up the listener for the end of the game, if host time reached 0 everyone should not be able to continue playing
     */
    public void listenEnded() {
        isEndedListener = new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                if (snapshot != null && snapshot.child("ended").getValue() != null && (boolean) snapshot.child("ended").getValue()) {
                    Game.endGame(localPlayer.getCollectedCoins().size(), localPlayer.getScore(), player.getPlayerId(), game.getPlayers(), MapActivity.this, false);
                }
            }

            @Override
            public void onCancelled(@NonNull DatabaseError error) {
                Log.e(TAG, error.getMessage());
            }
        };
        proxyG.addGameListener(game, isEndedListener);
    }


    @Override
    public void onBackPressed() {
    }
}