Arthelh/NCNF

View on GitHub
app/src/main/java/com/ncnf/utilities/map/MapHandler.java

Summary

Maintainability
B
4 hrs
Test Coverage
A
93%
package com.ncnf.utilities.map;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentManager;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.maps.android.clustering.Cluster;
import com.google.maps.android.clustering.ClusterItem;
import com.google.maps.android.clustering.ClusterManager;
import com.google.maps.android.clustering.view.DefaultClusterRenderer;
import com.ncnf.R;
import com.ncnf.models.Organization;
import com.ncnf.repositories.EventRepository;
import com.ncnf.repositories.OrganizationRepository;
import com.ncnf.utilities.settings.Settings;
import com.ncnf.models.Event;
import com.ncnf.models.SocialObject;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import static com.ncnf.utilities.StringCodes.DEBUG_TAG;

public class MapHandler {

    private final Activity context;
    private final GoogleMap mMap;
    private final EventRepository eventRepository;
    private final OrganizationRepository organizationRepository;

    private ClusterManager<CustomMapMarker> clusterManager;

    //Event cache to avoid querying every time we switch from organizers to events
    private List<Event> cache;

    // Indicate whether Events or Venues are shown. If false -> venues are shown
    private boolean eventsShown = true;
    private final float ZOOM_LEVEL = 13;

    /**
     * Creates a new MapHandler, the structure that takes care of displaying the markers correctly
     * and handling the retrieval of the events and organizations to be displayed
     * @param context The activity the map will be displayed in
     * @param mMap The Google Map to display
     * @param fragmentManager A children fragment manager from the MapFragment
     * @param eventRepository the service to query the database
     */
    public MapHandler(AppCompatActivity context, GoogleMap mMap, FragmentManager fragmentManager, EventRepository eventRepository, OrganizationRepository organizationRepository){
        this.context = context;
        this.mMap = mMap;
        this.eventRepository = eventRepository;
        this.organizationRepository = organizationRepository;

        if (mMap != null) { //This is just for MapHandler Unit test
            MarkerInfoWindowManager markerInfoWindowManager = new MarkerInfoWindowManager(context, context.getWindow(), fragmentManager);

            this.mMap.moveCamera(CameraUpdateFactory.zoomTo(ZOOM_LEVEL));

            this.clusterManager = new ClusterManager<>(context, mMap);
            this.clusterManager.setRenderer(new CustomRender<>(context, mMap, this.clusterManager));
            this.mMap.setInfoWindowAdapter(this.clusterManager.getMarkerManager());

            this.clusterManager.getMarkerCollection().setInfoWindowAdapter(markerInfoWindowManager);
            this.clusterManager.setOnClusterItemClickListener(markerInfoWindowManager);
            this.clusterManager.setOnClusterItemInfoWindowClickListener(markerInfoWindowManager);

            this.mMap.setOnCameraIdleListener(this.clusterManager);
            this.mMap.setOnMarkerClickListener(this.clusterManager);
            this.mMap.setOnInfoWindowClickListener(this.clusterManager);
        }
    }

    /**
     * Switches between event markers and organizer markers on the map. Must be given the map as argument
     * @param mMap The Google Map on which the markers are displayed
     */
    public void switchMarkers(GoogleMap mMap) {
        eventsShown = !eventsShown;
        clusterManager.clearItems();
        if (eventsShown) {
            queryAndAddEvents();
            mMap.setContentDescription("MAP_WITH_EVENTS");
        } else {
            queryAndAddOrgs();
            mMap.setContentDescription("MAP_WITH_VENUES");
        }
        clusterManager.cluster();
    }

    /**
     * Displays the markers on the map according to current settings
     */
    public void show_markers(){

        mMap.moveCamera(CameraUpdateFactory.newLatLng(Settings.getUserPosition()));

        if (eventsShown){
            queryAndAddEvents();
        } else {
            queryAndAddOrgs();
        }
        clusterManager.cluster();
    }

    /**
     * Updates the markers on the map. This method should be called after a change that
     * will modify which markers will appear on the map
     */
    public void update_markers(){
        clusterManager.clearItems();
        show_markers();
        clusterManager.cluster();
    }

    /**
     * Add the marker on the map for each event in the list (either its own marker or marker for a cluster of event)
     * @param events List of events we want to add an marker
     */
    private void addEventMarkers(List<Event> events){
        Map<LatLng, List<Event>> eventMap = new HashMap<>();
        for (Event p : events) {
            LatLng event_position = new LatLng(p.getLocation().getLatitude(), p.getLocation().getLongitude());

            //Additional check in range as geoqueries sometimes have false positives (https://cloud.google.com/firestore/docs/solutions/geoqueries#javaandroid_1)
            if (MapUtilities.position_in_range(event_position, Settings.getUserPosition()) && Settings.dateInRange(p.getDate().toLocalDate())){
                if (!eventMap.containsKey(event_position)){
                    eventMap.put(event_position, new ArrayList<>());
                }
                eventMap.get(event_position).add(p);
            }
        }
        Set<LatLng> keys = eventMap.keySet();
        for (LatLng k : keys){
            List<Event> list = eventMap.get(k);
            StringBuilder desc = new StringBuilder();
            for (SocialObject p : list){
                desc.append(p.getName()).append("\n");
            }
            String description = desc.toString();
            clusterManager.addItem(new CustomMapMarker(k, description, eventMap.get(k).get(0).getAddress(), list, null, true));
        }
        clusterManager.cluster();
    }

    private void addOrganizationMarkers(List<Organization> list){
        for (Organization o : list) {
            LatLng venue_position = new LatLng(o.getLocation().getLatitude(), o.getLocation().getLongitude());
            if (MapUtilities.position_in_range(venue_position, Settings.getUserPosition())){
                clusterManager.addItem(new CustomMapMarker(venue_position, o.getName(), o.getAddress(), new ArrayList<>(), o, false));
            }
        }
        clusterManager.cluster();
    }

    /**
     * Fetch the user's nearby events
     */
    private void queryAndAddEvents(){
        final List<Event> result = new ArrayList<>();

        ProgressBar spinner = context.findViewById(R.id.map_spinner);
        spinner.setVisibility(View.VISIBLE);

        CompletableFuture<List<Event>> completableFuture = eventRepository.getEventsNearBy();
        completableFuture.thenAccept(eventList -> {

            result.addAll(eventList);
            spinner.setVisibility(View.GONE);
            addEventMarkers(result);

        }).exceptionally(e -> {

            Log.d(DEBUG_TAG, e.getMessage());
            spinner.setVisibility(View.GONE);
            return null;

        });
    }

    private void queryAndAddOrgs(){
        final List<Organization> result = new ArrayList<>();

        ProgressBar spinner = context.findViewById(R.id.map_spinner);
        spinner.setVisibility(View.VISIBLE);

        CompletableFuture<List<Organization>> completableFuture = organizationRepository.getOrganizationsNearby();
        completableFuture.thenAccept(organizations -> {

            result.addAll(organizations);
            spinner.setVisibility(View.GONE);
            addOrganizationMarkers(result);

        }).exceptionally(e -> {

            Log.d(DEBUG_TAG, e.getMessage());
            spinner.setVisibility(View.GONE);
            return null;

        });
    }

    /**
     * Set the clustering of markers on the map
     */
    private static class CustomRender<T extends ClusterItem> extends DefaultClusterRenderer<T>{
        public CustomRender(Context context, GoogleMap map, ClusterManager clusterManager){
            super(context, map, clusterManager);
        }

        @Override
        protected boolean shouldRenderAsCluster(@NonNull Cluster<T> cluster) {
            return cluster.getSize() > 1;
        }

    }
}