com.luke.lukef.lukeapp.fragments.MapViewFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.luke.lukef.lukeapp.fragments.MapViewFragment.java

Source

/*
    BalticApp, for studying and tracking the condition of the Baltic sea
    and Gulf of Finland throug user submissions.
    Copyright (C) 2016  Daniel Zakharin, LuKe
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/> or
    the beginning of MainActivity.java file.
    
*/

package com.luke.lukef.lukeapp.fragments;

import android.Manifest;
import android.app.AlertDialog;
import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.constraint.ConstraintLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.Gravity;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.DatePicker;
import android.widget.ImageButton;
import android.widget.PopupWindow;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.GoogleMap.OnCameraIdleListener;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.VisibleRegion;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterManager;
import com.google.maps.android.clustering.view.DefaultClusterRenderer;
import com.google.maps.android.ui.IconGenerator;
import com.google.maps.android.ui.SquareTextView;
import com.luke.lukef.lukeapp.Constants;
import com.luke.lukef.lukeapp.MainActivity;
import com.luke.lukef.lukeapp.R;

import com.luke.lukef.lukeapp.model.ClusterMarkerAdmin;
import com.luke.lukef.lukeapp.popups.AdminMarkerPopup;
import com.luke.lukef.lukeapp.tools.SubmissionDatabase;
import com.luke.lukef.lukeapp.WelcomeActivity;
import com.luke.lukef.lukeapp.model.SessionSingleton;
import com.luke.lukef.lukeapp.model.ClusterMarker;
import com.luke.lukef.lukeapp.popups.SubmissionPopup;
import com.luke.lukef.lukeapp.tools.LukeUtils;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

/**
 * Handles the Map view, fetches submissions, populates map with Submissions and Admin markers.
 * <p>
 *     Using Google Maps requires a {@link MapFragment}. When the Fragment is created a conntection
 *     is made to Google api then the map needs to be instantiated using
 *     {@link MapFragment#getMapAsync(OnMapReadyCallback)}. When this is done, the method
 *     returns a {@link GoogleMap} object in the {@link OnMapReadyCallback#onMapReady(GoogleMap)}
 *     method. This Object can be sotred in a variable and is what is used when interacting with
 *     the Google Map.
 * </p>
 * <p>
 *     The map is populated with Submissions from the SQLite database to avoid unnecessary web traffic.
 *     Only submissions whose coordinates are in the part of the map that is visible to the user
 *     get displayed on the map, to avoid lagging up the map. This is done in the onCameraIdle method,
 *     since we don't want to be loading submissions while the map is still being moved.
 * </p>
 * <p>
 *      Filtering by date happens in the Calendar popup. When a date is selected, the map is cleared
 *      and then repopupalted.
 * </p>
 * <p>
 *     Setting up the Clustering of map markers happens in this class. This is done with a custom
 *     ClusterManager. This allows to change cluster colors and borders.
 * </p>
 */

// TODO: 25.1.2017 Add ContextCompat to MainActivity so it can work in older android version
public class MapViewFragment extends Fragment implements View.OnClickListener, OnMapReadyCallback,
        OnCameraIdleListener, GoogleApiClient.OnConnectionFailedListener, GoogleApiClient.ConnectionCallbacks,
        com.google.android.gms.location.LocationListener, GoogleMap.OnMarkerClickListener,
        ClusterManager.OnClusterClickListener<ClusterMarker>,
        ClusterManager.OnClusterItemClickListener<ClusterMarker> {
    private static final String TAG = "MapViewFragment";
    private View fragmentView;
    private Location lastLoc;
    private Location lastKnownLoc;
    private GoogleMap googleMap;
    private ClusterManager<ClusterMarker> clusterManager;
    private MapFragment mapFragment;
    private VisibleRegion visibleRegion;
    private List<String> submissionMarkerIdList;
    private GoogleApiClient googleApiClient;
    private LocationRequest locationRequest;
    private ImageButton menuButton;
    private ImageButton filtersButon;
    private ImageButton newSubmissionButton;
    private SubmissionPopup submissionPopup;
    private long minDateInMs = 0;
    private int tempY;
    private int tempM;
    private int tempD;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        this.submissionMarkerIdList = new ArrayList<>();

        connectToGoogleApi();
        setupGoogleMap();
        createLocationRequest();

        if (this.fragmentView != null) {
            ViewGroup parent = (ViewGroup) this.fragmentView.getParent();
            if (parent != null)
                parent.removeView(this.fragmentView);
        }
        //this is needed to avoid all sorts of crashing with the map when goin back and forth between fragments
        try {
            if (this.fragmentView == null)
                this.fragmentView = inflater.inflate(R.layout.fragment_map, container, false);
            this.mapFragment = ((MapFragment) getChildFragmentManager().findFragmentById(R.id.googleMapFragment));
            //check if fragment is null. On API 19 childfragmentmanager returns null, so regular fragment manager is used
            if (this.mapFragment == null) {
                this.mapFragment = ((MapFragment) getFragmentManager().findFragmentById(R.id.googleMapFragment));
            }
            this.mapFragment.getMapAsync(this);

        } catch (InflateException e) {
            Log.e(TAG, "onCreateView: ", e);
            //this.mapFragment.getMapAsync(this);
        }
        setupButtons();
        return this.fragmentView;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        //this prevents crash on older devices due to duplicate fragments
        MapFragment f = (MapFragment) getFragmentManager().findFragmentById(R.id.googleMapFragment);
        if (f != null)
            getFragmentManager().beginTransaction().remove(f).commit();
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
        case R.id.button_back:
            getMainActivity().openDrawer();
            break;
        case R.id.button_filters:
            showCalendarPicker();
            break;
        case R.id.button_new_submission:
            activateNewSubmission();
            break;
        }
    }

    /**
     * Returns the previously known user location, either the last location or last known location
     *
     * @return Location object, either long press location, last known location or null if no location is available.
     */
    private Location getLastLoc() {
        Location location = new Location("");
        if (this.lastLoc != null) {
            location.setAltitude(this.lastLoc.getAltitude());
            location.setLatitude(this.lastLoc.getLatitude());
            location.setLongitude(this.lastLoc.getLongitude());
            return location;
        } else if (this.lastKnownLoc != null) {
            location.setAltitude(this.lastKnownLoc.getAltitude());
            location.setLatitude(this.lastKnownLoc.getLatitude());
            location.setLongitude(this.lastKnownLoc.getLongitude());
            return location;
        } else {
            return null;
        }
    }

    /**
     * Switches to {@link NewSubmissionFragment} if user is logged in, GPS is on and there is a internet connection. Else
     * shows pop up to login if not.
     */
    private void activateNewSubmission() {
        if (SessionSingleton.getInstance().isUserLogged()) {
            if (LukeUtils.checkGpsStatus(getMainActivity())) {
                if (LukeUtils.checkInternetStatus(getMainActivity())) {
                    switchToNewSubmissions(constructBundle(getLastLoc()));
                } else {
                    // TODO: 21.1.2017 prompt user to turn on gps / internet
                    getMainActivity().makeToast("No internet connection");
                }
            } else {
                getMainActivity().makeToast("Activate GPS");
            }
        } else {
            createLoginPrompt();
        }
    }

    private void switchToNewSubmissions(Bundle bundle) {
        getMainActivity().fragmentSwitcher(Constants.fragmentTypes.FRAGMENT_NEW_SUBMISSION, bundle);
    }

    /**
     * Creates a {@link Bundle} object with doubles longitude, latitude and altitude.
     *
     * @param location Location object from which longitude, latitude and altitude are fetched
     *                 to be placed in the Bundle.
     * @return Bundle with double type values longitude, latitude and altitude.
     */
    private Bundle constructBundle(Location location) {
        Bundle bundle = new Bundle();
        bundle.putDouble("latitude", location.getLatitude());
        bundle.putDouble("longitude", location.getLongitude());
        bundle.putDouble("altitude", location.getAltitude());
        return bundle;
    }

    /**
     * Creates a login prompt with <b>Login</b> and <b>Cancel</b> buttons. Login button takes user to the starting screen
     */
    // TODO: 21.1.2017 change the style of the prompt to match style of the App
    // TODO: 21.1.2017 move to LukeUtils
    private void createLoginPrompt() {
        AlertDialog.Builder builder = new AlertDialog.Builder(getMainActivity());
        builder.setMessage("Please Log in to Submit").setCancelable(false)
                .setPositiveButton("Login", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        getActivity().startActivity(
                                new Intent(getActivity().getApplicationContext(), WelcomeActivity.class));
                    }
                }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    /**
     * Unhides the submissions popup
     */
    public void unhidePopup() {
        this.submissionPopup.unHidePopup();
    }

    private MainActivity getMainActivity() {
        return (MainActivity) getActivity();
    }

    /**
     * Handles setting up and click listeners
     */
    private void setupButtons() {
        this.filtersButon = (ImageButton) this.fragmentView.findViewById(R.id.button_filters);
        this.newSubmissionButton = (ImageButton) this.fragmentView.findViewById(R.id.button_new_submission);
        this.menuButton = (ImageButton) this.fragmentView.findViewById(R.id.button_back);
        this.filtersButon.setOnClickListener(this);
        this.newSubmissionButton.setOnClickListener(this);
        this.menuButton.setOnClickListener(this);
    }

    /**
     * Sets up the Google Map Fragment
     */
    private void setupGoogleMap() {
        LocationManager lm = (LocationManager) getMainActivity().getSystemService(Context.LOCATION_SERVICE);
        //check for permissions and get last known location of the device
        if (ActivityCompat.checkSelfPermission(getMainActivity(),
                Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
                && ActivityCompat.checkSelfPermission(getMainActivity(),
                        Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            getMainActivity().checkPermissions();
            this.lastKnownLoc = lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
        } else {
            this.lastKnownLoc = lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
        }
    }

    /**
     * Zooms the map on the last known location
     */
    private void zoomMap() {
        Location location = fetchArgsFromBundle();
        if (location != null) {
            CameraPosition cameraPosition = new CameraPosition.Builder()
                    .target(new LatLng(location.getLatitude(), location.getLongitude())) // Sets the center of the map to Mountain View
                    .zoom(17) // Sets the tilt of the luke_camera to 30 degrees
                    .build(); // Creates a CameraPosition from the builder
            googleMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
        }
    }

    /**
     * Fetches arguments from given bundle or calls {@link MapViewFragment#getLastLoc()} for
     * previously known location.
     *
     * @return {@link Location} object
     */
    private Location fetchArgsFromBundle() {
        Bundle b = getArguments();
        if (b != null && b.containsKey("latitude")) {
            Location location = new Location("jes");
            location.setLatitude(b.getDouble("latitude"));
            location.setLongitude(b.getDouble("longitude"));
            getArguments().clear();
            return location;
        } else if (getLastLoc() != null) {
            Location location = new Location("jes");
            location.setLatitude(getLastLoc().getLatitude());
            location.setLongitude(getLastLoc().getLongitude());
            return location;
        } else {
            return null;
        }
    }

    /**
     * Setup method for Marker clustering
     */
    private void setupClustering() {
        if (this.clusterManager == null) {
            this.clusterManager = new ClusterManager<>(getActivity(), this.googleMap);
            CompositeOnCameraIdleListener compositeOnCameraIdleListener = new CompositeOnCameraIdleListener();
            CompositeOnMarkerClickListener compositeOnMarkerClickListener = new CompositeOnMarkerClickListener();
            this.googleMap.setOnCameraIdleListener(compositeOnCameraIdleListener);
            this.googleMap.setOnMarkerClickListener(compositeOnMarkerClickListener);
            compositeOnCameraIdleListener.registerListener(this.clusterManager);
            compositeOnCameraIdleListener.registerListener(this);
            compositeOnMarkerClickListener.registerMarkerOnClickListener(this.clusterManager);
            compositeOnMarkerClickListener.registerMarkerOnClickListener(this);
            this.clusterManager.setOnClusterClickListener(this);
            this.clusterManager.setOnClusterItemClickListener(this);
            this.clusterManager.setRenderer(new MarkerRenderer(getActivity(), this.googleMap, this.clusterManager));
        } else {
            this.clusterManager.clearItems();
            this.submissionMarkerIdList.clear();
        }
    }

    /**
     * Handles connecting to the Google API
     */
    private void connectToGoogleApi() {
        this.googleApiClient = new GoogleApiClient.Builder(getMainActivity()).addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this).addApi(LocationServices.API).build();
        this.googleApiClient.connect();
    }

    /**
     * Makes a locationrequest to track the location of the user on the map
     */
    protected void createLocationRequest() {
        this.locationRequest = new LocationRequest();
        this.locationRequest.setInterval(5000);
        this.locationRequest.setFastestInterval(2000);
        this.locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
    }

    /**
     * Adds {@link ClusterMarker} admin markers into the map as basic markers that won't be grouped.
     * Calls the SQLite {@link SubmissionDatabase#queryAdminMarkers()} for a cursor with admin markers
     */
    private void addAdminMarkersToMap() {
        SubmissionDatabase submissionDatabase = new SubmissionDatabase(getActivity());
        Cursor queryCursor = submissionDatabase.queryAdminMarkers();
        queryCursor.moveToFirst();
        if (queryCursor.getCount() > 0) {
            do {
                if (!this.submissionMarkerIdList
                        .contains(queryCursor.getString(queryCursor.getColumnIndexOrThrow("admin_marker_id")))) {
                    ClusterMarkerAdmin adminMarker = new ClusterMarkerAdmin(
                            queryCursor.getString(queryCursor.getColumnIndexOrThrow("admin_marker_id")),
                            queryCursor.getDouble(queryCursor.getColumnIndexOrThrow("admin_marker_latitude")),
                            queryCursor.getDouble(queryCursor.getColumnIndexOrThrow("admin_marker_longitude")),
                            queryCursor.getString(queryCursor.getColumnIndexOrThrow("admin_marker_title")), "");
                    this.submissionMarkerIdList
                            .add(queryCursor.getString(queryCursor.getColumnIndexOrThrow("admin_marker_id")));
                    this.clusterManager.addItem(adminMarker);
                }

            } while (queryCursor.moveToNext());
            this.clusterManager.cluster();
        }
        submissionDatabase.closeDbConnection();
    }

    /**
     * Adds submissions to the map based on the provided <code>VisibleRegion</code>. Passes the VisibleRegion
     * to the {@link SubmissionDatabase#querySubmissions(VisibleRegion, Long)}
     *
     * @param visibleRegion The region currently visible on the map
     */
    private void addSubmissionsToMap(VisibleRegion visibleRegion) {
        SubmissionDatabase submissionDatabase = new SubmissionDatabase(getActivity());
        Cursor queryCursor;
        if (this.minDateInMs > 0) {
            queryCursor = submissionDatabase.querySubmissions(visibleRegion, this.minDateInMs);
        } else {
            queryCursor = submissionDatabase.querySubmissions(visibleRegion, null);
        }
        queryCursor.moveToFirst();
        if (queryCursor.getCount() > 0) {
            do {
                if (!this.submissionMarkerIdList
                        .contains(queryCursor.getString(queryCursor.getColumnIndexOrThrow("submission_id")))) {
                    ClusterMarker clusterMarker = new ClusterMarker(
                            queryCursor.getString(queryCursor.getColumnIndexOrThrow("submission_id")),
                            queryCursor.getDouble(queryCursor.getColumnIndexOrThrow("submission_latitude")),
                            queryCursor.getDouble(queryCursor.getColumnIndexOrThrow("submission_longitude")), "",
                            queryCursor.getString(queryCursor.getColumnIndexOrThrow("submission_positive")));
                    this.submissionMarkerIdList
                            .add(queryCursor.getString(queryCursor.getColumnIndexOrThrow("submission_id")));
                    this.clusterManager.addItem(clusterMarker);
                }
            } while (queryCursor.moveToNext());
            this.clusterManager.cluster();
        }
        submissionDatabase.closeDbConnection();
    }

    /**
     * Handles showing the Calendar pop up, fetching the selected date, calling to fetch
     * submissions again
     */
    private void showCalendarPicker() {
        // Inflate the popup_layout.xml
        ConstraintLayout viewGroup = (ConstraintLayout) getMainActivity().findViewById(R.id.popup_calendar_root);
        LayoutInflater layoutInflater = (LayoutInflater) getMainActivity()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        final View layout = layoutInflater.inflate(R.layout.popup_calendar, viewGroup);
        // Some offset to align the popup a bit to the right, and a bit down, relative to button's position.
        //or if popup is on edge display it to the left of the circle
        Display display = getMainActivity().getWindowManager().getDefaultDisplay();
        Point size = new Point(0, 0);
        display.getSize(size);

        int OFFSET_X = 25;
        int OFFSET_Y = 25;

        final DatePicker dP = (DatePicker) layout.findViewById(R.id.popup_calendar_datepicker);

        // Creating the PopupWindow
        final PopupWindow popup = new PopupWindow(getMainActivity());
        popup.setAnimationStyle(android.R.style.Animation_Dialog);
        popup.setContentView(layout);

        popup.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
        popup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);

        popup.setFocusable(true);
        //gets rid of default background
        popup.setBackgroundDrawable(new BitmapDrawable(getMainActivity().getResources(), (Bitmap) null));
        //popup.setBackgroundDrawable(new BitmapDrawable(getMainActivity().getResources(), (Bitmap) nu));

        // Displaying the popup at the specified location, + offsets.
        popup.showAtLocation(layout, Gravity.NO_GRAVITY, 200 + OFFSET_X, 300 + OFFSET_Y);
        Calendar minDate;
        minDate = Calendar.getInstance();
        this.tempY = minDate.get(Calendar.YEAR);
        this.tempM = minDate.get(Calendar.MONTH);
        this.tempD = minDate.get(Calendar.DAY_OF_MONTH);
        dP.init(minDate.get(Calendar.YEAR), minDate.get(Calendar.MONTH), minDate.get(Calendar.DAY_OF_MONTH),
                new DatePicker.OnDateChangedListener() {
                    @Override
                    // Months start from 0, so January is month 0
                    public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
                        tempY = year;
                        tempM = monthOfYear;
                        tempD = dayOfMonth;
                        Log.e(TAG, "onDateChanged: selected " + tempD + " " + tempM + " " + tempY);
                    }
                });
        ImageButton okButton = (ImageButton) layout.findViewById(R.id.popup_calendar_accept);
        ImageButton cancelButton = (ImageButton) layout.findViewById(R.id.popup_calendar_cancel);
        okButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Calendar calendar = Calendar.getInstance();
                calendar.set(tempY, tempM, tempD, 1, 0);
                Log.e(TAG, "onClick: calendar time in ms " + calendar.getTimeInMillis());
                // clear items from clustermanager and submissionMarkerList, as all new submissions
                // need to be fetched based on the selected date
                clusterManager.clearItems();
                submissionMarkerIdList.clear();
                addAdminMarkersToMap();
                setMinDateInMs(calendar.getTimeInMillis());
                popup.dismiss();
            }
        });

        cancelButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                setMinDateInMs(0);
                popup.dismiss();

            }
        });
    }

    /**
     * Setter for the minDateInMs which is the minimum date of which submissions should be shown
     *
     * @param minDateInMs The minimum date of which submissions are shown, in MS
     */
    private void setMinDateInMs(long minDateInMs) {
        this.minDateInMs = minDateInMs;
        if (this.minDateInMs > 0) {
            addSubmissionsToMap(this.googleMap.getProjection().getVisibleRegion());
        } else {
            addSubmissionsToMap(this.googleMap.getProjection().getVisibleRegion());
        }
    }

    @Override
    public boolean onClusterClick(Cluster<ClusterMarker> cluster) {
        // TODO: 12/12/2016 Zoom when clicking a cluster
        /*   // Zoom in the cluster. Need to create LatLngBounds and including all the cluster items
           // inside of bounds, then animate to center of the bounds.
            
           // Create the builder to collect all essential cluster items for the bounds.
           LatLngBounds.Builder builder = LatLngBounds.builder();
           for (ClusterItem item : cluster.getItems()) {
        builder.include(item.getPosition());
           }
           // Get the LatLngBounds
           final LatLngBounds bounds = builder.build();
            
        *//*     CameraPosition cameraPosition = new CameraPosition.Builder()
               .target(cluster.getPosition())      // Sets the center of the map to Mountain View
               .zoom(15)                   // Sets the zoom
               .bearing(90)                // Sets the orientation of the camera to east
               .tilt(30)                   // Sets the tilt of the camera to 30 degrees
               .build();                   // Creates a CameraPosition from the builder
              googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
           *//*
                  
                    Log.e(TAG, "onClusterClick: BOUNDS SW " + bounds.southwest + " Cneter " + bounds.getCenter());
                    // Animate camera to the bounds
                    try {
              this.googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100));
              //this.googleMap.animateCamera(CameraUpdateFactory.zoomTo(15));
              Log.e(TAG, "onClusterClick: animated");
                    } catch (Exception e) {
              Log.e(TAG, "onClusterClick: animate failed ", e);
                    }*/

        return false;
    }

    /**
     * Checks if the clicked submission is an admin marker and passes
     * correct parameters to SubmissionPopup.
     *
     * @param clusterMarker Clicked cluster marker
     * @return false
     */
    @Override
    public boolean onClusterItemClick(ClusterMarker clusterMarker) {
        boolean isAdminMarker = true;
        if (clusterMarker instanceof ClusterMarkerAdmin) {
            AdminMarkerPopup adminMarkerPopup = new AdminMarkerPopup(clusterMarker.getSubmissionId(),
                    getMainActivity());
            adminMarkerPopup.createPopupAdmin();
        } else {
            //deprecated code left in because of time constraints, should remove isAdminmarker boolean from submissionpopup
            isAdminMarker = false;
            submissionPopup = new SubmissionPopup(getMainActivity());
            submissionPopup.createPopup(clusterMarker.getSubmissionId(), isAdminMarker);
        }
        return false;
    }

    @Override
    public boolean onMarkerClick(Marker marker) {
        Log.e(TAG, "onMarkerClick: marker clicked");
        return false;
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        this.googleMap = googleMap;
        setupClustering();
        this.googleMap.getUiSettings().setZoomControlsEnabled(true);
        this.googleMap.setOnCameraIdleListener(this);
        this.googleMap.getUiSettings().setMapToolbarEnabled(false);
        zoomMap();
        if (ActivityCompat.checkSelfPermission(getMainActivity(),
                Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
                && ActivityCompat.checkSelfPermission(getMainActivity(),
                        Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        this.googleMap.setMyLocationEnabled(true);
        this.googleMap.setOnMapLongClickListener(new GoogleMap.OnMapLongClickListener() {
            @Override
            public void onMapLongClick(LatLng latLng) {
                if (SessionSingleton.getInstance().isUserLogged()) {
                    if (LukeUtils.checkInternetStatus(getMainActivity())) {
                        Location location = new Location("");
                        location.setLatitude(latLng.latitude);
                        location.setLongitude(latLng.longitude);
                        switchToNewSubmissions(constructBundle(location));
                    } else {
                        getMainActivity().makeToast("You need to log in to do this");
                    }
                } else {
                    createLoginPrompt();
                }
            }
        });
    }

    @Override
    public void onStop() {
        googleApiClient.disconnect();
        super.onStop();
    }

    /**
     * Gets called when user has stopped moving the map, filters visibler submissions depending on the visible map area
     */
    @Override
    public void onCameraIdle() {
        if (this.googleMap != null) {
            if (this.visibleRegion == null) {
                // get visible region
                this.visibleRegion = this.googleMap.getProjection().getVisibleRegion();
                // if a filter has been set then pass the filter, if not the null
                if (this.minDateInMs > 0) {
                    addSubmissionsToMap(this.visibleRegion);
                } else {
                    addSubmissionsToMap(this.visibleRegion);
                }
                // adds admin markers to the map regardless of filters or region
                addAdminMarkersToMap();
            } else {
                // TODO: 29/11/2016 check here if the luke_camera has moved enough to get new stuff from the DB or not
                this.visibleRegion = this.googleMap.getProjection().getVisibleRegion();
                if (this.minDateInMs > 0) {
                    addSubmissionsToMap(this.visibleRegion);
                } else {
                    addSubmissionsToMap(this.visibleRegion);
                }
                addAdminMarkersToMap();
            }
        }
        // TODO: 02/12/2016 ungroup markers
        /*
                if (this.googleMap.getCameraPosition().zoom < 5) {
                }
        */
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        Log.e(TAG, "onConnectionFailed: connection failed");
    }

    @Override
    public void onConnected(@Nullable Bundle bundle) {
        Log.e(TAG, "onConnected: connected to google api");

        if (ActivityCompat.checkSelfPermission(getMainActivity(),
                Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
                && ActivityCompat.checkSelfPermission(getMainActivity(),
                        Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            // TODO: Consider calling
            //    ActivityCompat#requestPermissions
            // here to request the missing permissions, and then overriding
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
            //                                          int[] grantResults)
            // to handle the case where the user grants the permission. See the documentation
            // for ActivityCompat#requestPermissions for more details.
            getMainActivity().checkPermissions();
            return;
        }
        this.lastKnownLoc = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
        if (this.lastKnownLoc != null) {
            this.lastLoc = this.lastKnownLoc;
        }

        LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, this);
    }

    @Override
    public void onConnectionSuspended(int i) {
        Log.e(TAG, "onConnectionSuspended: google api connection suspended");
    }

    @Override
    public void onLocationChanged(Location location) {
        this.lastLoc = location;
    }

    /**
     * Provides objects possibility to listen to OnCameraIdle events by calling
     * {@link #registerListener(OnCameraIdleListener listener)}
     */
    class CompositeOnCameraIdleListener implements OnCameraIdleListener {
        private List<OnCameraIdleListener> registeredListeners = new ArrayList<>();

        /**
         * Adds OnCameraIdleListener type object to the <code>List<OnCameraIdleListener> registeredListeners</code>.
         *
         * @param listener OnCameraIdleListener type object
         */
        void registerListener(OnCameraIdleListener listener) {
            this.registeredListeners.add(listener);
        }

        @Override
        public void onCameraIdle() {
            // loop through listeners and call their onCameraIdle method()
            for (int i = 0; i < this.registeredListeners.size(); i++) {
                this.registeredListeners.get(i).onCameraIdle();
            }
        }
    }

    /**
     * Provides objects possibility to listen to OnCameraIdle events by calling
     * {@link #registerMarkerOnClickListener(GoogleMap.OnMarkerClickListener)}.
     */
    class CompositeOnMarkerClickListener implements GoogleMap.OnMarkerClickListener {
        private List<GoogleMap.OnMarkerClickListener> registeredListeners = new ArrayList<>();

        /**
         * Adds OnMarkerClickListener type object to the <code>List<OnMarkerClickListener> registeredListeners</code>
         *
         * @param listener OnCameraIdleListener type object
         */
        void registerMarkerOnClickListener(GoogleMap.OnMarkerClickListener listener) {
            this.registeredListeners.add(listener);
        }

        @Override
        public boolean onMarkerClick(Marker marker) {
            // loop through listeners and call their onCameraIdle method()
            for (int i = 0; i < this.registeredListeners.size(); i++) {
                this.registeredListeners.get(i).onMarkerClick(marker);
            }
            return false;
        }
    }

    /**
     * Custom renderer for the markers and clusters, makes it possible to change markers and clusters
     * colors and icons
     */
    private class MarkerRenderer extends DefaultClusterRenderer<ClusterMarker> {
        private final IconGenerator mIconGenerator;
        private ShapeDrawable mColoredCircleBackground;
        private SparseArray mIcons = new SparseArray();
        private final float mDensity;

        MarkerRenderer(Context context, GoogleMap map, ClusterManager<ClusterMarker> clusterManager) {
            super(context, map, clusterManager);
            this.mDensity = context.getResources().getDisplayMetrics().density;
            this.mIconGenerator = new IconGenerator(context);
            this.mIconGenerator.setContentView(this.makeSquareTextView(context));
            this.mIconGenerator.setTextAppearance(com.google.maps.android.R.style.amu_ClusterIcon_TextAppearance);
            this.mIconGenerator.setBackground(this.makeClusterBackground(R.color.quill_gray));
        }

        /**
         * Called before a cluster item is rendered, changes marker color based on marker type
         *
         * @param item          The clicked cluster marker
         * @param markerOptions Setup object for the marker
         */
        @Override
        protected void onBeforeClusterItemRendered(ClusterMarker item, MarkerOptions markerOptions) {
            // change marker color based on the marker values
            if (!item.getAdminMarkerTitle().isEmpty()) {
                BitmapDescriptor markerDescriptor = BitmapDescriptorFactory
                        .defaultMarker(BitmapDescriptorFactory.HUE_ORANGE);
                markerOptions.icon(markerDescriptor);
            } else if (item.getPositive().equals("true")) {
                BitmapDescriptor markerDescriptor = BitmapDescriptorFactory
                        .defaultMarker(BitmapDescriptorFactory.HUE_GREEN);
                markerOptions.icon(markerDescriptor);
            } else if (item.getPositive().equals("false")) {
                BitmapDescriptor markerDescriptor = BitmapDescriptorFactory
                        .defaultMarker(BitmapDescriptorFactory.HUE_RED);
                markerOptions.icon(markerDescriptor);
            } else if (item.getPositive().equals("neutral")) {
                BitmapDescriptor markerDescriptor = BitmapDescriptorFactory
                        .defaultMarker(BitmapDescriptorFactory.HUE_BLUE);
                markerOptions.icon(markerDescriptor);
            }
        }

        @Override
        protected void onClusterItemRendered(ClusterMarker clusterItem, Marker marker) {
            super.onClusterItemRendered(clusterItem, marker);
        }

        /**
         * Called before a cluster is rendered, checks through the cluster and based on contents
         * colors the cluster and it's borders
         *
         * @param cluster       The cluster that's going to be rendered
         * @param markerOptions Options object for the cluster
         */
        @Override
        protected void onBeforeClusterRendered(Cluster<ClusterMarker> cluster, MarkerOptions markerOptions) {
            // set default cluster border color
            this.mIconGenerator.setBackground(this.makeClusterBackground(R.color.storm_dust_gray));
            // check if cluster has admin marker inside and change circle outline color if it has
            for (ClusterMarker marker : cluster.getItems()) {
                if (!marker.getAdminMarkerTitle().isEmpty()) {
                    this.mIconGenerator.setBackground(this.makeClusterBackground(R.color.super_red));
                    break;
                }
            }
            int clusterColor;
            findElementWithMostOccurrences(cluster);
            // Set cluster color based on what items there's the most
            switch (findElementWithMostOccurrences(cluster)) {
            case POSITIVE:
                clusterColor = getMainActivity().getContextCompatColor(R.color.marker_positive);
                break;
            case NEUTRAL:
                clusterColor = getMainActivity().getContextCompatColor(R.color.marker_neutral);
                break;
            case NEGATIVE:
                clusterColor = getMainActivity().getContextCompatColor(R.color.marker_negative);
                break;
            default:
                clusterColor = getMainActivity().getContextCompatColor(R.color.marker_neutral);
                break;
            }

            int bucket = this.getBucket(cluster);
            //BitmapDescriptor descriptor = this.mIcons.get(bucket);
            this.mColoredCircleBackground.getPaint().setColor(clusterColor);
            BitmapDescriptor descriptor = BitmapDescriptorFactory
                    .fromBitmap(this.mIconGenerator.makeIcon(this.getClusterText(bucket)));
            this.mIcons.put(bucket, descriptor);
            markerOptions.icon(descriptor);
        }

        /**
         * Finds the element type with most occurrences in the cluster
         *
         * @param cluster Cluster of SubmissionMarkers
         * @return Constants.markerCategories enum value matching the most common elements
         */
        private Constants.markerCategories findElementWithMostOccurrences(Cluster<ClusterMarker> cluster) {
            int negative = 0;
            int neutral = 0;
            int positive = 0;

            double clusterSize = ((double) cluster.getSize()) / 2;
            for (ClusterMarker marker : cluster.getItems()) {
                switch (marker.getPositive()) {
                case "false":
                    negative++;
                    break;
                case "neutral":
                    neutral++;
                    break;
                case "true":
                    positive++;
                    break;
                }
                if (negative > clusterSize || neutral > clusterSize || positive > clusterSize) {
                    break;
                }
            }

            int biggest = Math.max(negative, Math.max(neutral, positive));
            if (neutral == biggest) {
                return Constants.markerCategories.NEUTRAL;
            } else if (negative == biggest) {
                return Constants.markerCategories.NEGATIVE;
            } else if (positive == biggest) {
                return Constants.markerCategories.POSITIVE;
            } else {
                return Constants.markerCategories.NEUTRAL;
            }
        }

        private SquareTextView makeSquareTextView(Context context) {
            SquareTextView squareTextView = new SquareTextView(context);
            ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(-2, -2);
            squareTextView.setLayoutParams(layoutParams);
            // changed text
            squareTextView.setId(com.google.maps.android.R.id.amu_text);
            int twelveDpi = (int) (12.0F * this.mDensity);
            squareTextView.setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi);
            return squareTextView;
        }

        /**
         * Defines the cluster background, including outline, shape and color.
         *
         * @param borderColor Color of the cluster border
         * @return Returns type <code>LayerDrawable</code> background for the cluster
         */
        private LayerDrawable makeClusterBackground(int borderColor) {
            // Outline color
            int clusterOutlineColor = getMainActivity().getContextCompatColor(borderColor);
            this.mColoredCircleBackground = new ShapeDrawable(new OvalShape());
            ShapeDrawable outline = new ShapeDrawable(new OvalShape());
            outline.getPaint().setColor(clusterOutlineColor);
            LayerDrawable background = new LayerDrawable(new Drawable[] { outline, this.mColoredCircleBackground });
            int strokeWidth = (int) (this.mDensity * 3.0F);
            background.setLayerInset(1, strokeWidth, strokeWidth, strokeWidth, strokeWidth);
            return background;
        }
    }
}