com.andrew67.ddrfinder.activity.MapViewer.java Source code

Java tutorial

Introduction

Here is the source code for com.andrew67.ddrfinder.activity.MapViewer.java

Source

/*
 * Copyright (c) 2013 Luis Torres
 * Web: https://github.com/ltorres8890/Clima
 * 
 * Copyright (c) 2013-2016 Andrs Cordero
 * Web: https://github.com/Andrew67/DdrFinder
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.andrew67.ddrfinder.activity;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import com.andrew67.ddrfinder.R;
import com.andrew67.ddrfinder.adapters.MapLoader;
import com.andrew67.ddrfinder.adapters.MapLoaderV1;
import com.andrew67.ddrfinder.adapters.MapLoaderV3;
import com.andrew67.ddrfinder.interfaces.ArcadeLocation;
import com.andrew67.ddrfinder.interfaces.DataSource;
import com.andrew67.ddrfinder.interfaces.MessageDisplay;
import com.andrew67.ddrfinder.interfaces.ProgressBarController;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.lsjwzh.widget.materialloadingprogressbar.CircleProgressBar;

import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.Toast;

public class MapViewer extends FragmentActivity
        implements ProgressBarController, MessageDisplay, OnMapReadyCallback {

    public static final int BASE_ZOOM = 12;
    private static final int PERMISSIONS_REQUEST_LOCATION = 1;

    private GoogleMap mMap;
    private MenuItem reloadButton;
    private CircleProgressBar progressBar;

    private final Map<Marker, ArcadeLocation> currentMarkers = new HashMap<>();
    // Set as ArrayList instead of List due to Bundle packing
    private final ArrayList<LatLngBounds> loadedAreas = new ArrayList<>();

    /**
     * Loaded data sources, keyed by source name
     */
    private final Map<String, DataSource> loadedSources = new HashMap<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        PreferenceManager.setDefaultValues(this, R.xml.preferences, false);

        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        setContentView(R.layout.map_viewer);

        progressBar = (CircleProgressBar) findViewById(R.id.progressBar);
        onCreateSavedInstanceState = savedInstanceState;

        ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMapAsync(this);
    }

    /**
     * Finalize initialization steps that depend on GoogleMap (previously in onCreate).
     */
    private Bundle onCreateSavedInstanceState = null;

    @Override
    public void onMapReady(GoogleMap googleMap) {
        mMap = googleMap;

        // Check for location permission, and request if disabled
        // This permission allows the user to locate themselves on the map
        if (ContextCompat.checkSelfPermission(this,
                android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            mMap.setMyLocationEnabled(true);
            if (onCreateSavedInstanceState == null) {
                zoomToCurrentLocation();
            }
        } else {
            ActivityCompat.requestPermissions(this,
                    new String[] { android.Manifest.permission.ACCESS_FINE_LOCATION },
                    PERMISSIONS_REQUEST_LOCATION);
        }

        // Restore previously loaded areas locations, and sources if available
        // (and re-create the location markers)
        if (onCreateSavedInstanceState != null && onCreateSavedInstanceState.containsKey("loadedAreas")
                && onCreateSavedInstanceState.containsKey("loadedLocations")
                && onCreateSavedInstanceState.containsKey("loadedSources")) {
            final ArrayList<LatLngBounds> savedLoadedAreas = onCreateSavedInstanceState
                    .getParcelableArrayList("loadedAreas");
            if (savedLoadedAreas != null)
                loadedAreas.addAll(savedLoadedAreas);
            final ArrayList<ArcadeLocation> savedLoadedLocations = onCreateSavedInstanceState
                    .getParcelableArrayList("loadedLocations");

            final ArrayList<DataSource> savedLoadedSources = onCreateSavedInstanceState
                    .getParcelableArrayList("loadedSources");
            if (savedLoadedSources != null) {
                for (DataSource src : savedLoadedSources) {
                    loadedSources.put(src.getShortName(), src);
                }
            }

            MapLoader.fillMap(mMap, currentMarkers, savedLoadedLocations);
        }

        mMap.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() {

            @Override
            public void onCameraChange(CameraPosition position) {
                if (isAutoloadEnabled()) {
                    updateMap(false);
                }
            }
        });
        mMap.setOnMarkerClickListener(actionModeEnabler);
        mMap.setOnMapClickListener(actionModeDisabler);
        mMap.setOnInfoWindowClickListener(moreInfoListener);
    }

    /**
     * Whether the autoload flag is enabled for map scrolling.
      */
    private boolean isAutoloadEnabled() {
        return PreferenceManager.getDefaultSharedPreferences(this).getBoolean(SettingsActivity.KEY_PREF_AUTOLOAD,
                true);
    }

    /**
     * Load arcade locations into the map
     * @param force Whether to ignore already loaded areas and load them again
     */
    private void updateMap(boolean force) {
        LatLngBounds box = mMap.getProjection().getVisibleRegion().latLngBounds;
        final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        if (force || !alreadyLoaded(box)) {
            // Preload a slightly larger box area when zoomed in, for a smoother pan/zoom experience
            // as network requests are reduced.
            if (Math.abs(box.northeast.latitude - box.southwest.latitude) < 0.5
                    && Math.abs(box.northeast.longitude - box.southwest.longitude) < 0.5) {
                box = LatLngBounds.builder()
                        .include(new LatLng(box.northeast.latitude + 0.125, box.northeast.longitude + 0.125))
                        .include(new LatLng(box.southwest.latitude - 0.125, box.southwest.longitude - 0.125))
                        .build();
            }

            final int version = Integer.parseInt(sharedPref.getString(SettingsActivity.KEY_PREF_API_VERSION, ""));
            switch (version) {
            case SettingsActivity.API_V11:
                new MapLoaderV1(mMap, currentMarkers, this, this, loadedAreas, loadedSources, sharedPref)
                        .execute(box);
                break;
            case SettingsActivity.API_V30:
                new MapLoaderV3(mMap, currentMarkers, this, this, loadedAreas, loadedSources, sharedPref)
                        .execute(box);
                break;
            default:
                showMessage(R.string.error_api_ver);
                Log.d("MapViewer", "unsupported API version requested: " + version);
                break;
            }

        }
    }

    /**
     * Test whether the given boundaries have already been loaded
     * @param box Bounding box
     * @return Whether data for the boundaries has been loaded
     */
    private boolean alreadyLoaded(LatLngBounds box) {
        // Test all four corners (best we can do)
        final LatLng northeast = box.northeast;
        final LatLng southwest = box.southwest;
        final LatLng northwest = new LatLng(northeast.latitude, southwest.longitude);
        final LatLng southeast = new LatLng(southwest.latitude, northeast.longitude);

        boolean loaded = false;
        boolean loadedNE = false;
        boolean loadedSW = false;
        boolean loadedNW = false;
        boolean loadedSE = false;

        for (LatLngBounds bounds : loadedAreas) {
            if (bounds.contains(northeast))
                loadedNE = true;
            if (bounds.contains(southwest))
                loadedSW = true;
            if (bounds.contains(northwest))
                loadedNW = true;
            if (bounds.contains(southeast))
                loadedSE = true;
            if (loadedNE && loadedSW && loadedNW && loadedSE)
                break;
        }
        if (loadedNE && loadedSW && loadedNW && loadedSE)
            loaded = true;

        return loaded;
    }

    /**
     * Clears map of all markers, and internal data structures of all loaded areas.
     */
    private void clearMap() {
        mMap.clear();
        currentMarkers.clear();
        loadedAreas.clear();
    }

    /**
     * Get the source corresponding to the given location.
     * If specific source not found, "fallback" source is returned.
     * @return Data source of location.
     */
    private DataSource getSource(ArcadeLocation location) {
        if (loadedSources.containsKey(location.getSrc()))
            return loadedSources.get(location.getSrc());
        else {
            Log.d("MapViewer", "failed to get source: " + location.getSrc());
            return loadedSources.get("fallback");
        }
    }

    /**
     * Zooms and moves the map to the user's last known current location, typically on app startup.
     */
    private void zoomToCurrentLocation() {
        final LocationManager locationManager = (LocationManager) this.getSystemService(LOCATION_SERVICE);
        Location lastKnown = null;
        try {
            lastKnown = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
        } catch (SecurityException e) {
            showMessage(R.string.error_perm_loc);
        }
        if (lastKnown != null) {
            mMap.animateCamera(CameraUpdateFactory
                    .newLatLngZoom(new LatLng(lastKnown.getLatitude(), lastKnown.getLongitude()), BASE_ZOOM));
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        // Save the list of currently loaded map areas and locations
        outState.putParcelableArrayList("loadedAreas", loadedAreas);
        final ArrayList<ArcadeLocation> loadedLocations = new ArrayList<>(currentMarkers.size());
        loadedLocations.addAll(currentMarkers.values());
        outState.putParcelableArrayList("loadedLocations", loadedLocations);

        // Save the map of currently loaded sources, as a list
        final ArrayList<DataSource> currSources = new ArrayList<>(loadedSources.size());
        currSources.addAll(loadedSources.values());
        outState.putParcelableArrayList("loadedSources", currSources);
    }

    @Override
    public void showProgressBar() {
        progressBar.setVisibility(View.VISIBLE);
        if (reloadButton != null)
            reloadButton.setVisible(false);
    }

    @Override
    public void hideProgressBar() {
        if (reloadButton != null)
            reloadButton.setVisible(true);
        progressBar.setVisibility(View.INVISIBLE);
    }

    @Override
    public void showMessage(String message) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }

    // Keep toast references mapped by resource id, to prevent excessive repeated toasts.
    private Map<Integer, Toast> toasts = new HashMap<>();

    @Override
    public void showMessage(int resourceId) {
        final Toast oldToast = toasts.get(resourceId);
        if (oldToast == null) {
            final Toast newToast = Toast.makeText(this, resourceId, Toast.LENGTH_SHORT);
            toasts.put(resourceId, newToast);
            newToast.show();
        } else {
            oldToast.setText(resourceId);
            oldToast.show();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        reloadButton = menu.findItem(R.id.action_reload);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.action_reload:
            updateMap(true);
            return true;
        case R.id.action_about:
            startActivity(new Intent(this, About.class));
            return true;
        case R.id.action_settings:
            // Store current preferences for future comparison in onResume.
            prevPrefs = PreferenceManager.getDefaultSharedPreferences(this).getAll();
            startActivity(new Intent(this, SettingsActivity.class));
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    private Map<String, ?> prevPrefs = null;

    @Override
    protected void onResume() {
        super.onResume();
        // Clear all markers and reload current view when a preference changed.
        final Map<String, ?> currPrefs = PreferenceManager.getDefaultSharedPreferences(this).getAll();
        if (prevPrefs != null && !currPrefs.equals(prevPrefs)) {
            clearMap();
            if (isAutoloadEnabled()) {
                updateMap(false);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
            @NonNull int[] grantResults) {
        switch (requestCode) {
        case PERMISSIONS_REQUEST_LOCATION:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                try {
                    mMap.setMyLocationEnabled(true);
                } catch (SecurityException e) {
                    /* Satisfy linter; it should be granted */ }
                zoomToCurrentLocation();
            } else {
                showMessage(R.string.error_perm_loc);
            }
        }
    }

    /**
     * Listener class that activates the action bar on marker click.
     */
    private GoogleMap.OnMarkerClickListener actionModeEnabler = new GoogleMap.OnMarkerClickListener() {
        @Override
        public boolean onMarkerClick(Marker marker) {
            if (actionMode == null) {
                actionMode = MapViewer.this.startActionMode(actionModeCallback);
            }
            selectedMarker = marker;
            return false; // keep the default action of moving view and showing info window
        }
    };

    /**
     * Listener class that de-activates the action bar on clicking elsewhere.
     */
    private GoogleMap.OnMapClickListener actionModeDisabler = new GoogleMap.OnMapClickListener() {
        @Override
        public void onMapClick(LatLng coords) {
            if (actionMode != null) {
                actionMode.finish();
            }
        }
    };

    /**
     * Handles action mode creation, destruction, and actions.
     * Template: https://developer.android.com/guide/topics/ui/menus.html#CAB
     */
    private ActionMode actionMode = null;
    private Marker selectedMarker = null;
    private ActionMode.Callback actionModeCallback = new ActionMode.Callback() {
        // Called when the action mode is created; startActionMode() was called
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            // Inflate a menu resource providing context menu items
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.context_menu, menu);
            return true;
        }

        // Called each time the action mode is shown. Always called after onCreateActionMode, but
        // may be called multiple times if the mode is invalidated.
        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false; // Return false if nothing is done
        }

        // Called when the user selects a contextual menu item
        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            if (selectedMarker == null)
                return false;

            final ArcadeLocation selectedLocation = currentMarkers.get(selectedMarker);
            if (selectedLocation == null)
                return false;

            final LocationActions actions = new LocationActions(selectedLocation, getSource(selectedLocation));

            switch (item.getItemId()) {
            case R.id.action_navigate:
                actions.navigate(MapViewer.this);
                return true;
            case R.id.action_moreinfo:
                actions.moreInfo(MapViewer.this);
                return true;
            case R.id.action_copygps:
                actions.copyGps(MapViewer.this, MapViewer.this);
                return true;
            default:
                return false;
            }
        }

        // Called when the user exits the action mode
        @Override
        public void onDestroyActionMode(ActionMode mode) {
            actionMode = null;
            if (selectedMarker != null) {
                selectedMarker.hideInfoWindow();
            }
        }

    };

    /**
     * Info window click listener which triggers the "More Info" action.
     */
    private GoogleMap.OnInfoWindowClickListener moreInfoListener = new GoogleMap.OnInfoWindowClickListener() {
        @Override
        public void onInfoWindowClick(Marker marker) {
            final ArcadeLocation selectedLocation = currentMarkers.get(selectedMarker);
            if (selectedLocation != null) {
                final LocationActions actions = new LocationActions(selectedLocation, getSource(selectedLocation));
                actions.moreInfo(MapViewer.this);
            }
        }
    };
}