/* * 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(); } } }