ca.ualberta.cmput301w14t08.geochan.fragments.MapViewFragment.java Source code

Java tutorial

Introduction

Here is the source code for ca.ualberta.cmput301w14t08.geochan.fragments.MapViewFragment.java

Source

/*
 * Copyright 2014 Artem Chikin
 * Copyright 2014 Artem Herasymchuk
 * Copyright 2014 Tom Krywitsky
 * Copyright 2014 Henry Pabst
 * Copyright 2014 Bradley Simons
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ca.ualberta.cmput301w14t08.geochan.fragments;

import java.util.ArrayList;

import org.osmdroid.api.IMapController;
import org.osmdroid.bonuspack.clustering.GridMarkerClusterer;
import org.osmdroid.bonuspack.overlays.Marker;
import org.osmdroid.bonuspack.overlays.Marker.OnMarkerClickListener;
import org.osmdroid.bonuspack.overlays.MarkerInfoWindow;
import org.osmdroid.bonuspack.overlays.Polyline;
import org.osmdroid.bonuspack.routing.OSRMRoadManager;
import org.osmdroid.bonuspack.routing.Road;
import org.osmdroid.bonuspack.routing.RoadManager;
import org.osmdroid.bonuspack.routing.RoadNode;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;

import android.app.ProgressDialog;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import ca.ualberta.cmput301w14t08.geochan.R;
import ca.ualberta.cmput301w14t08.geochan.helpers.ErrorDialog;
import ca.ualberta.cmput301w14t08.geochan.helpers.LocationListenerService;
import ca.ualberta.cmput301w14t08.geochan.helpers.MapDataHelper;
import ca.ualberta.cmput301w14t08.geochan.models.Comment;
import ca.ualberta.cmput301w14t08.geochan.models.CustomMarker;
import ca.ualberta.cmput301w14t08.geochan.models.GeoLocation;

/**
 * A Fragment class for displaying Maps. The Map will display the locations of
 * each comment in the thread, and will center around the original post. It will
 * provide the feature of getting directions from the users current location to
 * the location of the original post.
 * 
 * @author Brad Simons
 * 
 */
public class MapViewFragment extends Fragment {

    private MapDataHelper mapData;
    private LocationListenerService locationListenerService;
    private CustomMarker originalPostMarker;
    private Polyline roadOverlay;
    private GridMarkerClusterer replyPostClusterMarkers;
    private GridMarkerClusterer directionsClusterMarkers;
    private GridMarkerClusterer startAndFinishClusterMarkers;
    private ArrayList<GridMarkerClusterer> clusterers;
    private ArrayList<CustomMarker> markers;
    private ArrayList<CustomMarker> replyMarkers;

    /**
     * Set up the fragment's UI.
     * 
     * @param inflater
     *            The LayoutInflater used to inflate the fragment's UI.
     * @param container
     *            The parent View that the fragment's UI is attached to.
     * @param savedInstanceState
     *            The previously saved state of the fragment.
     * @return The View for the fragment's UI.
     * 
     */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        setHasOptionsMenu(false);

        return inflater.inflate(R.layout.fragment_map_view, container, false);
    }

    /**
     * Inflates the menu and adds and add items to action bar if present.
     * 
     * @param menu
     *            The Menu object for the fragment.
     * @param inflater
     *            the MenuInflater for inflating the fragment's menu.
     */
    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        // Inflate the menu; this adds items to the action bar if it is present.
        MenuItem item = menu.findItem(R.id.action_settings);
        item.setVisible(true);
        super.onCreateOptionsMenu(menu, inflater);
    }

    /**
     * Initiates a location listener which immediately starts listening for
     * location updates. Gets the current location as well. Then unpacks the
     * bundle passed to the fragment. It then gets the map setup and prepares
     * the min and max latitude and longitude required to display the map
     * properly for calculation. Then finally sets the zoom level
     */
    @Override
    public void onStart() {
        super.onStart();

        locationListenerService = new LocationListenerService(getActivity());
        locationListenerService.startListening();

        Bundle args = getArguments();
        Comment topComment = (Comment) args.getParcelable("thread_comment");

        markers = new ArrayList<CustomMarker>();
        replyMarkers = new ArrayList<CustomMarker>();

        setupClusterGroups();

        GeoLocation geoLocation = topComment.getLocation();
        if (geoLocation.getLocation() == null) {
            ErrorDialog.show(getActivity(), "Thread has no location");
            FragmentManager fm = getFragmentManager();
            fm.popBackStackImmediate();
        } else {
            this.setupMap(topComment);
            this.setZoomLevel(topComment.getLocation());
        }
    }

    /**
     * Calls onStop in the superclass, and tells the locationListener to stop
     * listening.
     */
    @Override
    public void onStop() {
        super.onStop();
        locationListenerService.stopListening();
    }

    /**
     * Sets up cluster groups so that when many Markers over lap each other in
     * the same cluster group, a cluster image is displayed instead of the
     * markers. This cluster image also displays the number of Markers that it
     * represents
     */
    public void setupClusterGroups() {
        replyPostClusterMarkers = new GridMarkerClusterer(getActivity());
        directionsClusterMarkers = new GridMarkerClusterer(getActivity());
        startAndFinishClusterMarkers = new GridMarkerClusterer(getActivity());

        Drawable clusterIconD = getResources().getDrawable(R.drawable.marker_cluster);
        Bitmap clusterIcon = ((BitmapDrawable) clusterIconD).getBitmap();

        directionsClusterMarkers.setIcon(clusterIcon);
        replyPostClusterMarkers.setIcon(clusterIcon);
        startAndFinishClusterMarkers.setIcon(clusterIcon);

        clusterers = new ArrayList<GridMarkerClusterer>();
        clusterers.add(directionsClusterMarkers);
        clusterers.add(replyPostClusterMarkers);
        clusterers.add(startAndFinishClusterMarkers);
    }

    /**
     * This sets up the comment location the map. The map is centered at the
     * location of the comment GeoLocation, and places a pin at this point. It
     * then calls handleChildComments to place pins for each child comment in
     * the thread.
     * 
     * @param topComment
     *            The OP of the ThreadComment.
     */
    public void setupMap(Comment topComment) {
        mapData = new MapDataHelper((MapView) getActivity().findViewById(R.id.open_map_view));
        mapData.setUpMap();

        if (commentLocationIsValid(topComment)) {
            GeoLocation geoLocation = topComment.getLocation();
            Drawable icon = getResources().getDrawable(R.drawable.red_map_pin);
            originalPostMarker = new CustomMarker(geoLocation, mapData.getMap(), icon);
            originalPostMarker.setUpInfoWindow("OP", getActivity());

            setMarkerListeners(originalPostMarker);

            markers.add(originalPostMarker);
            startAndFinishClusterMarkers.add(originalPostMarker);

            handleChildComments(topComment);

            mapData.getOverlays().add(replyPostClusterMarkers);
            mapData.getOverlays().add(directionsClusterMarkers);
            mapData.getOverlays().add(originalPostMarker);
        }
        mapData.getMap().invalidate();
    }

    /**
     * Sets the default zoom level for the mapview. This takes the max and min
     * of both lat and long, and zooms to span the area required. It also
     * animates to the startGeoPoint, which is the location of the topComment.
     * The values must be padded with a zoom_factor, which is a static class
     * variable
     * 
     * @param geoLocation
     *            GeoLocation used to start the basis of the distance
     */
    public void setZoomLevel(GeoLocation geoLocation) {
        // get the mapController and set the zoom
        IMapController mapController = mapData.getController();

        int zoomFactor;
        int zoomSpan = calculateZoomSpan();

        // calculates the appropriate zoom level
        zoomFactor = 19 - (int) (Math.log10(zoomSpan) * 2.2);
        if (zoomFactor > 18 || zoomSpan < 1) {
            zoomFactor = 18;
        } else if (zoomFactor < 2) {
            zoomFactor = 2;
        }

        // set the zoom center
        mapController.setZoom(zoomFactor);
        mapController.animateTo(geoLocation.makeGeoPoint());
    }

    /**
     * Calculates the minimum and maximum values for latitude and longitude
     * between an array of GeoPoints. This is used to determine the zoom level.
     * 
     * @return The maximum distance between markers on the map.
     */
    private int calculateZoomSpan() {
        int opLat = originalPostMarker.getPosition().getLatitudeE6();
        int opLong = originalPostMarker.getPosition().getLongitudeE6();

        // To calculate the max and min latitude and longitude of all
        // the comments, we set the min's to max integer values and vice versa
        // then have values of each comment modify these variables
        int minLat = opLat;
        int maxLat = opLat;
        int minLong = opLong;
        int maxLong = opLong;

        // get max min lat long for replies
        for (CustomMarker marker : markers) {
            if (marker.getGeoLocation() != originalPostMarker.getGeoLocation()) {
                GeoPoint geoPoint = marker.getGeoPoint();
                int geoLat = geoPoint.getLatitudeE6();
                int geoLong = geoPoint.getLongitudeE6();

                maxLat = Math.max(geoLat, maxLat);
                minLat = Math.min(geoLat, minLat);
                maxLong = Math.max(geoLong, maxLong);
                minLong = Math.min(geoLong, minLong);
            }
        }
        int deltaLong = maxLong - minLong;
        int deltaLat = maxLat - minLat;
        return Math.max(deltaLong, deltaLat);
    }

    /**
     * Sets an onMarkerClickListener and onMarkerDragListener the marker passed
     * in. This is used to handle click events for the maps, which will cause
     * infoWindows to show and hide.
     * 
     * @param locationMarker
     *            Marker that the listeners will be attached to.
     */
    private void setMarkerListeners(Marker locationMarker) {

        locationMarker.setOnMarkerClickListener(new OnMarkerClickListener() {
            @Override
            public boolean onMarkerClick(Marker marker, MapView map) {
                if (marker.isInfoWindowShown() != true) {
                    hideInfoWindows();
                    marker.showInfoWindow();
                } else {
                    hideInfoWindows();
                }
                return false;
            }
        });
    }

    /**
     * Recursive method for handling all comments in the thread. First checks if
     * the comment has any children or not. If none, simply return. Otherwise,
     * call setGeoPointMarker for each child of the comment. Call
     * checkCommmentLocation to calculate the min and max of the lat and long
     * for the entire thread. Then finally make a recursive call to check if a
     * child comment has any children.
     * 
     * @param comment
     *            Comment to be added to the map.
     */
    private void handleChildComments(Comment comment) {
        ArrayList<Comment> children = comment.getChildren();
        if (children.size() == 0) {
            return;
        } else {
            for (Comment childComment : children) {
                GeoLocation commentLocation = childComment.getLocation();

                if (commentLocationIsValid(childComment)) {
                    Drawable icon = getResources().getDrawable(R.drawable.blue_map_pin);

                    CustomMarker replyMarker = new CustomMarker(commentLocation, mapData.getMap(), icon);
                    replyMarker.createInfoWindow();
                    replyMarker.setTitle("Reply");

                    if (commentLocation.getLocationDescription() != null) {
                        replyMarker.setSubDescription(commentLocation.getLocationDescription());
                    } else {
                        replyMarker.setSubDescription("Unknown Location");
                    }

                    setMarkerListeners(replyMarker);
                    replyPostClusterMarkers.add(replyMarker);

                    markers.add(replyMarker);
                    replyMarkers.add(replyMarker);

                    handleChildComments(childComment);
                }
            }
        }
    }

    /**
     * Hides infoWindows for every marker on the map
     */
    private void hideInfoWindows() {
        for (Marker marker : markers) {
            marker.hideInfoWindow();
        }
    }

    /**
     * Checks to see if a comment in the thread has valid GPS coordinates. Valid
     * coordinates are -90 < lat < 90, and -180 < longitude < 180. It also does
     * a null check on location.
     * 
     * @param comment
     *            to be check for valid location
     * @return boolean isValidLocation
     */
    public boolean commentLocationIsValid(Comment comment) {
        GeoLocation location = comment.getLocation();
        if (location.getLocation() == null) {
            return false;
        } else {
            return (location.getLatitude() >= -90.0 || location.getLatitude() <= 90.0
                    || location.getLongitude() >= -180.0 || location.getLongitude() <= 180.0);
        }
    }

    /**
     * Simply re-adds all the reply markers to the markers array. This
     * is used to refresh the map
     */
    private void addRepliesToMarkerArray() {
        for (CustomMarker marker : replyMarkers) {
            markers.add(marker);
        }
    }

    /**
     * Called when the get_directions_button is clicked. Displays directions
     * from the users current location to the comment location. Uses an Async
     * task to get map overlay. If the users current location cannot be
     * obtained, an error is shown to the screen and the async task is not
     * called
     */
    public void getDirections() {
        GeoLocation currentLocation = new GeoLocation(locationListenerService);

        if (currentLocation.getLocation() == null) {
            ErrorDialog.show(getActivity(), "Could not retrieve your location");
        } else {
            hideInfoWindows();

            directionsClusterMarkers.getItems().clear();
            startAndFinishClusterMarkers.getItems().clear();

            for (Marker marker : directionsClusterMarkers.getItems()) {
                if (markers.contains(marker)) {
                    markers.remove(marker);
                }
            }

            mapData.clearOverlays();
            mapData.refreshMap();

            new GetDirectionsAsyncTask().execute();
        }

        mapData.refreshMap();
    }

    /**
     * Async task class. This task is designed to retrieve directions from the
     * users current location to the location of the original post of the
     * thread. It displays a ProgressDialog while the location is being
     * retrieved.
     * 
     * @author Brad Simons
     */
    class GetDirectionsAsyncTask extends AsyncTask<Void, Void, Void> {

        ProgressDialog directionsLoadingDialog = new ProgressDialog(getActivity());

        /**
         * Displays a ProgessDialog while the task is executing
         */
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            directionsLoadingDialog.setMessage("Getting Directions");
            directionsLoadingDialog.show();
        }

        /**
         * Calculating the directions from the current to the location of the
         * topComment. Builds a road overlay and adds it to the openMapView
         * objects overlays.
         */
        @Override
        protected Void doInBackground(Void... params) {
            RoadManager roadManager = new OSRMRoadManager();
            ArrayList<GeoPoint> waypoints = new ArrayList<GeoPoint>();

            GeoLocation currentLocation = new GeoLocation(locationListenerService);

            waypoints.add(new GeoPoint(currentLocation.getLatitude(), currentLocation.getLongitude()));
            waypoints.add(originalPostMarker.getPosition());
            Road road = roadManager.getRoad(waypoints);

            roadOverlay = RoadManager.buildRoadOverlay(road, getActivity());

            Drawable nodeIcon = getResources().getDrawable(R.drawable.marker_node);
            Drawable nodeImage = getResources().getDrawable(R.drawable.ic_continue);

            for (int i = 0; i < road.mNodes.size(); i++) {
                RoadNode node = road.mNodes.get(i);
                GeoLocation geoLocation = new GeoLocation(node.mLocation);
                CustomMarker nodeMarker = new CustomMarker(geoLocation, mapData.getMap(), nodeIcon);

                MarkerInfoWindow infoWindow = new MarkerInfoWindow(R.layout.bonuspack_bubble, mapData.getMap());
                nodeMarker.setInfoWindow(infoWindow);

                nodeMarker.setTitle("Step " + i);
                nodeMarker.setSnippet(node.mInstructions);
                nodeMarker.setSubDescription(Road.getLengthDurationText(node.mLength, node.mDuration));
                nodeMarker.setImage(nodeImage);

                setMarkerListeners(nodeMarker);

                directionsClusterMarkers.add(nodeMarker);
                markers.add(nodeMarker);
            }

            return null;
        }

        /**
         * Task is now finished. Creates the current location marker and sets it
         * on the map. Clears the map and re-adds all the overlays to the map,
         * then refreshes the map
         */
        @Override
        protected void onPostExecute(Void result) {
            super.onPostExecute(result);
            directionsLoadingDialog.dismiss();

            GeoLocation currentLocation = new GeoLocation(locationListenerService);

            Drawable icon = getResources().getDrawable(R.drawable.green_map_pin);

            CustomMarker currentLocationMarker = new CustomMarker(currentLocation, mapData.getMap(), icon);
            currentLocationMarker.setUpInfoWindow("Current Location", getActivity());

            setMarkerListeners(currentLocationMarker);

            startAndFinishClusterMarkers.add(currentLocationMarker);
            startAndFinishClusterMarkers.add(originalPostMarker);

            markers.add(currentLocationMarker);
            markers.add(originalPostMarker);
            addRepliesToMarkerArray();

            mapData.getOverlays().clear();
            mapData.getOverlays().add(roadOverlay);
            mapData.getOverlays().add(directionsClusterMarkers);
            mapData.getOverlays().add(replyPostClusterMarkers);
            mapData.getOverlays().add(startAndFinishClusterMarkers);

            mapData.setZoom(15);
            mapData.setCenter(currentLocation.makeGeoPoint());

            mapData.refreshMap();
        }
    }

}