/* * Copyright (C) 2011-2014 Paul Watts (paulcwatts@gmail.com), * University of South Florida (sjbarbeau@gmail.com), and individual contributors. * * 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 org.onebusaway.android.map.googlemapsv2; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.location.Location; import android.os.Bundle; import android.os.Handler; import android.provider.Settings; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.Toast; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.LocationSource; import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.UiSettings; 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.Polyline; import com.google.android.gms.maps.model.PolylineOptions; import com.google.android.gms.maps.model.VisibleRegion; import org.onebusaway.android.R; import org.onebusaway.android.app.Application; import org.onebusaway.android.io.ObaApi; import org.onebusaway.android.io.elements.ObaReferences; import org.onebusaway.android.io.elements.ObaRegion; import org.onebusaway.android.io.elements.ObaRoute; import org.onebusaway.android.io.elements.ObaShape; import org.onebusaway.android.io.elements.ObaStop; import org.onebusaway.android.io.request.ObaResponse; import org.onebusaway.android.io.request.ObaTripsForRouteResponse; import org.onebusaway.android.map.DirectionsMapController; import org.onebusaway.android.map.MapModeController; import org.onebusaway.android.map.MapParams; import org.onebusaway.android.map.RouteMapController; import org.onebusaway.android.map.StopMapController; import org.onebusaway.android.region.ObaRegionsTask; import org.onebusaway.android.util.LocationHelper; import org.onebusaway.android.util.LocationUtils; import org.onebusaway.android.util.PreferenceUtils; import org.onebusaway.android.util.UIUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.concurrent.TimeUnit; /** * The MapFragment class is split into two basic modes: * stop mode and route mode. It needs to be able to switch * between the two. * <p/> * So this class handles the common functionality between * the two modes: zooming, the options menu, * saving/restoring state, and other minor bookkeeping * (the stop user map). * <p/> * Everything else is handed off to a specific * MapFragmentController instance. * * @author paulw, barbeau */ public class BaseMapFragment extends SupportMapFragment implements MapModeController.Callback, ObaRegionsTask.Callback, MapModeController.ObaMapView, LocationSource, LocationHelper.Listener, com.google.android.gms.maps.GoogleMap.OnCameraChangeListener, StopOverlay.OnFocusChangedListener, OnMapReadyCallback, VehicleOverlay.Controller { public static final String TAG = "BaseMapFragment"; private static final int REQUEST_NO_LOCATION = 41; // // Location Services and Maps API v2 constants // public static final float CAMERA_DEFAULT_ZOOM = 16.0f; public static final float DEFAULT_MAP_PADDING_DP = 20.0f; // Keep track of current map padding private int mMapPaddingLeft = 0; private int mMapPaddingTop = 0; private int mMapPaddingRight = 0; private int mMapPaddingBottom = 0; // Use fully-qualified class name to avoid import statement, because it interferes with scripted // copying of Maps API v2 classes between Google/Amazon build flavors (see #254) private com.google.android.gms.maps.GoogleMap mMap; private String mFocusStopId; // The Fragment controls the stop overlay, since that // is used by both modes. private StopOverlay mStopOverlay; private VehicleOverlay mVehicleOverlay; // We only display the out of range dialog once private boolean mWarnOutOfRange = true; private boolean mRunning = false; private MapModeController mController; private ArrayList<Polyline> mLineOverlay = new ArrayList<Polyline>(); // Markers that are added to the map by classes external to this map package private SimpleMarkerOverlay mSimpleMarkerOverlay; // We have to convert from LatLng to Location, so hold references to both private LatLng mCenter; private Location mCenterLocation; private OnLocationChangedListener mListener; // Listen to map tap events OnFocusChangedListener mOnFocusChangedListener; // Listen to map loading/progress bar events OnProgressBarChangedListener mOnProgressBarChangedListener; LocationHelper mLocationHelper; Bundle mLastSavedInstanceState; public interface OnFocusChangedListener { /** * Called when a stop on the map is clicked (i.e., tapped), which sets focus to a stop, * or when the user taps on an area away from the map for the first time after a stop * is already selected, which removes focus * * @param stop the ObaStop that obtained focus, or null if no stop is in focus * @param routes a HashMap of all route display names that serve this stop - key is * routeId * @param location the user touch location on the map */ void onFocusChanged(ObaStop stop, HashMap<String, ObaRoute> routes, Location location); } public interface OnProgressBarChangedListener { /** * Called when the map is loading information. If showProgressBar is true, then the map is * loading information and the progress bar should be shown, but if it's false, then the * map * is finished loading information and the progress bar should be hidden. * * @param showProgressBar true if the map is loading information and the progress bar * should * be shown, false if the map is finished loading information and * the * progress bar should be hidden. */ void onProgressBarChanged(boolean showProgressBar); } public static BaseMapFragment newInstance() { return new BaseMapFragment(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = super.onCreateView(inflater, container, savedInstanceState); mLocationHelper = new LocationHelper(getActivity()); mLocationHelper.registerListener(this); if (MapHelpV2.isMapsInstalled(getActivity())) { // Save the savedInstanceState mLastSavedInstanceState = savedInstanceState; // Register for an async callback when the map is ready getMapAsync(this); } else { MapHelpV2.promptUserInstallMaps(getActivity()); } // If we have a recent location, show this while we're waiting on the LocationHelper Location l = Application .getLastKnownLocation(getActivity(), mLocationHelper.getGoogleApiClient()); if (l != null) { final long TIME_THRESHOLD = TimeUnit.MINUTES.toMillis(5); if (System.currentTimeMillis() - l.getTime() < TIME_THRESHOLD) { onLocationChanged(l); } } return v; } @Override public void onMapReady(com.google.android.gms.maps.GoogleMap map) { mMap = map; initMap(mLastSavedInstanceState); } private void initMap(Bundle savedInstanceState) { UiSettings uiSettings = mMap.getUiSettings(); // Show the location on the map mMap.setMyLocationEnabled(true); // Set location source mMap.setLocationSource(this); // Listener for camera changes mMap.setOnCameraChangeListener(this); // Hide MyLocation button on map, since we have our own button uiSettings.setMyLocationButtonEnabled(false); // Hide Zoom controls uiSettings.setZoomControlsEnabled(false); // Hide Toolbar uiSettings.setMapToolbarEnabled(false); // Instantiate class that holds generic markers to be added by outside classes mSimpleMarkerOverlay = new SimpleMarkerOverlay(mMap); if (savedInstanceState != null) { initMapState(savedInstanceState); } else { Bundle args = getActivity().getIntent().getExtras(); // The rest of this code assumes a bundle exists, even if it's empty if (args == null) { args = new Bundle(); } double lat = args.getDouble(MapParams.CENTER_LAT, 0.0d); double lon = args.getDouble(MapParams.CENTER_LON, 0.0d); if (lat == 0.0d && lon == 0.0d) { // Try to restore the latest map view location PreferenceUtils.maybeRestoreMapViewToBundle(args); } initMapState(args); } } private void initMapState(Bundle args) { mFocusStopId = args.getString(MapParams.STOP_ID); mMapPaddingLeft = args.getInt(MapParams.MAP_PADDING_LEFT, MapParams.DEFAULT_MAP_PADDING); mMapPaddingTop = args.getInt(MapParams.MAP_PADDING_TOP, MapParams.DEFAULT_MAP_PADDING); mMapPaddingRight = args.getInt(MapParams.MAP_PADDING_RIGHT, MapParams.DEFAULT_MAP_PADDING); mMapPaddingBottom = args .getInt(MapParams.MAP_PADDING_BOTTOM, MapParams.DEFAULT_MAP_PADDING); setPadding(mMapPaddingLeft, mMapPaddingTop, mMapPaddingRight, mMapPaddingBottom); String mode = args.getString(MapParams.MODE); if (mode == null) { mode = MapParams.MODE_STOP; } setMapMode(mode, args); } @Override public void onDestroy() { if (mController != null) { mController.destroy(); } super.onDestroy(); } @Override public void onPause() { if (mLocationHelper != null) { mLocationHelper.onPause(); } if (mController != null) { mController.onPause(); } Location center = getMapCenterAsLocation(); if (center != null) { PreferenceUtils.saveMapViewToPreferences(center.getLatitude(), center.getLongitude(), getZoomLevelAsFloat()); } mRunning = false; super.onPause(); } /** * This is called when fm.beginTransaction().hide() or fm.beginTransaction().show() is called * * @param hidden True if the fragment is now hidden, false if it is not visible. */ @Override public void onHiddenChanged(boolean hidden) { if (mController != null) { mController.onHidden(hidden); } super.onHiddenChanged(hidden); } @Override public void onResume() { if (mLocationHelper != null) { mLocationHelper.onResume(); } mRunning = true; if (mController != null) { mController.onResume(); } super.onResume(); } @Override public void onSaveInstanceState(Bundle outState) { if (mController != null) { mController.onSaveInstanceState(outState); } outState.putString(MapParams.MODE, getMapMode()); outState.putString(MapParams.STOP_ID, mFocusStopId); Location center = getMapCenterAsLocation(); if (mMap != null) { outState.putDouble(MapParams.CENTER_LAT, center.getLatitude()); outState.putDouble(MapParams.CENTER_LON, center.getLongitude()); outState.putFloat(MapParams.ZOOM, getZoomLevelAsFloat()); } outState.putInt(MapParams.MAP_PADDING_LEFT, mMapPaddingLeft); outState.putInt(MapParams.MAP_PADDING_TOP, mMapPaddingTop); outState.putInt(MapParams.MAP_PADDING_RIGHT, mMapPaddingRight); outState.putInt(MapParams.MAP_PADDING_BOTTOM, mMapPaddingBottom); } @Override public void onViewStateRestored(Bundle savedInstanceState) { if (mController != null) { mController.onViewStateRestored(savedInstanceState); } super.onViewStateRestored(savedInstanceState); } public boolean isRouteDisplayed() { return (mController != null) && MapParams.MODE_ROUTE.equals(mController.getMode()); } /** * Initialize the Stop Overlay * * @return true if the overlay was successfully initialized, false if it was not */ public boolean setupStopOverlay() { if (mStopOverlay != null) { // Overlay was previously initialized and can be used return true; } if (mMap == null) { // We need a map reference to initialize the overlay return false; } mStopOverlay = new StopOverlay(getActivity(), mMap); mStopOverlay.setOnFocusChangeListener(this); return true; } public void setupVehicleOverlay() { Activity a = getActivity(); if (mVehicleOverlay == null && a != null) { mVehicleOverlay = new VehicleOverlay(a, mMap); mVehicleOverlay.setController(this); } } protected void showDialog(int id) { MapDialogFragment.newInstance(id, this).show(getFragmentManager(), MapDialogFragment.TAG); } // @Override // public void onActivityResult(int requestCode, int resultCode, Intent data) { // super.onActivityResult(requestCode, resultCode, data); // switch (requestCode) { // case REQUEST_NO_LOCATION: // // Clear the map center so we can get the user's location again // setMyLocation(); // break; // } // } // // Fragment Controller // @Override public String getMapMode() { if (mController != null) { return mController.getMode(); } return null; } @Override public void setMapMode(String mode, Bundle args) { String oldMode = getMapMode(); if (oldMode != null && oldMode.equals(mode)) { mController.setState(args); return; } if (mController != null) { mController.destroy(); } if (mStopOverlay != null) { mStopOverlay.clear(false); } if (MapParams.MODE_ROUTE.equals(mode)) { mController = new RouteMapController(this); } else if (MapParams.MODE_STOP.equals(mode)) { mController = new StopMapController(this); } else if (MapParams.MODE_DIRECTIONS.equals(mode)) { mController = new DirectionsMapController(this); } mController.setState(args); mController.onResume(); } @Override public MapModeController.ObaMapView getMapView() { // We implement the ObaMapView interface too (since we're using MapFragment) return this; } /** * Adds a generic marker to the map and returns the ID associated with that marker, which can * be used to remove the marker via removeMarker() * * @param location Location at which the marker should be added * @param hue The hue (color) of the marker. Value must be greater or equal to 0 and less than 360, or null if the default color should be used. * @return the ID associated with the marker that was just added, or -1 if the addition failed */ @Override public int addMarker(Location location, Float hue) { if (mSimpleMarkerOverlay == null) { return -1; } return mSimpleMarkerOverlay.addMarker(location, hue); } /** * Removes the marker from the map that has the given ID, which was previously generated by * addMarker() in this class * * @param markerId the ID for the marker that should be removed from the map */ @Override public void removeMarker(int markerId) { if (mSimpleMarkerOverlay == null) { return; } mSimpleMarkerOverlay.removeMarker(markerId); } /** * Define a visible region on the map, to signal to the map that portions of the map around * the edges may be obscured, by setting padding on each of the four edges of the map. * * @param left the number of pixels of padding to be added on the left of the map, or null * if the existing padding should be used * @param top the number of pixels of padding to be added on the top of the map, or null * if the existing padding should be used * @param right the number of pixels of padding to be added on the right of the map, or null * if the existing padding should be used * @param bottom the number of pixels of padding to be added on the bottom of the map, or null * if the existing padding should be used */ @Override public void setPadding(Integer left, Integer top, Integer right, Integer bottom) { if (left != null) { mMapPaddingLeft = left; } if (top != null) { mMapPaddingTop = top; } if (right != null) { mMapPaddingRight = right; } if (bottom != null) { mMapPaddingBottom = bottom; } if (mMap != null) { mMap.setPadding(mMapPaddingLeft, mMapPaddingTop, mMapPaddingRight, mMapPaddingBottom); } } @Override public void showProgress(boolean show) { if (mOnProgressBarChangedListener != null) { mOnProgressBarChangedListener.onProgressBarChanged(show); } } @Override public void showStops(List<ObaStop> stops, ObaReferences refs) { // Make sure that the stop overlay has been successfully initialized if (setupStopOverlay() && stops != null) { mStopOverlay.populateStops(stops, refs); } } @Override public void notifyOutOfRange() { //Before we trigger the out of range warning, make sure we have region info //or have a API URL that was custom set by the user in via Preferences //Otherwise, its premature since we don't know the device's relationship to //available OBA regions or the manually set API region String serverName = Application.get().getCustomApiUrl(); if (mWarnOutOfRange && (Application.get().getCurrentRegion() != null || !TextUtils .isEmpty(serverName))) { if (mRunning && UIUtils.canManageDialog(getActivity())) { showDialog(MapDialogFragment.OUTOFRANGE_DIALOG); } } } // // Region Task Callback // @Override public void onRegionTaskFinished(boolean currentRegionChanged) { if (!isAdded()) { // Too early or late in the Fragment lifecycle to take any action return; } Location l = Application .getLastKnownLocation(getActivity(), mLocationHelper.getGoogleApiClient()); // If the region changed, and we don't have a location or the map center is still (0,0), // then zoom to the region Location mapCenter = getMapCenterAsLocation(); if (currentRegionChanged && (l == null || (mapCenter != null && mapCenter.getLatitude() == 0.0 && mapCenter.getLongitude() == 0.0))) { zoomToRegion(); } } public void setOnFocusChangeListener(OnFocusChangedListener onFocusChangedListener) { mOnFocusChangedListener = onFocusChangedListener; } public void setOnProgressBarChangedListener( OnProgressBarChangedListener onProgressBarChangedListener) { mOnProgressBarChangedListener = onProgressBarChangedListener; } // // Stop changed handler // final Handler mStopChangedHandler = new Handler(); public void onFocusChanged(final ObaStop stop, final HashMap<String, ObaRoute> routes, final Location location) { // Run in a separate thread, to avoid blocking UI for long running events mStopChangedHandler.post(new Runnable() { public void run() { if (stop != null) { mFocusStopId = stop.getId(); //Log.d(TAG, "Focused changed to " + stop.getName()); } else { mFocusStopId = null; //Log.d(TAG, "Removed focus"); } // Pass overlay focus event up to listeners for this fragment if (mOnFocusChangedListener != null) { mOnFocusChangedListener.onFocusChanged(stop, routes, location); } } }); } /** * Sets the map view to the last available location * * @param useDefaultZoom true if the CAMERA_DEFAULT_ZOOM should be used, false if the * current * zoom level should be kept * @param animateToLocation true if the map should animate the transition to the new view, or * false if it should snap to the new view without animation * @return true if there was a a location to set the map view to, false if there was not */ @Override @SuppressWarnings("deprecation") public boolean setMyLocation(boolean useDefaultZoom, boolean animateToLocation) { if (!LocationUtils.isLocationEnabled(getActivity()) && mRunning && UIUtils.canManageDialog( getActivity())) { // If the user hasn't opted out of "Enable location" dialog, show it to them SharedPreferences prefs = Application.getPrefs(); if (!prefs.getBoolean(getString(R.string.preference_key_never_show_location_dialog), false)) { showDialog(MapDialogFragment.NOLOCATION_DIALOG); } return false; } GoogleApiClient apiClient = null; if (mLocationHelper != null) { apiClient = mLocationHelper.getGoogleApiClient(); } Location lastLocation = Application.getLastKnownLocation(getActivity(), apiClient); if (lastLocation == null) { Toast.makeText(getActivity(), getResources() .getString(R.string.main_waiting_for_location), Toast.LENGTH_SHORT).show(); return false; } setMyLocation(lastLocation, useDefaultZoom, animateToLocation); return true; } private void setMyLocation(Location l, boolean useDefaultZoom, boolean animateToLocation) { if (mMap != null) { // Move camera to current location CameraPosition.Builder cameraPosition = new CameraPosition.Builder() .target(MapHelpV2.makeLatLng(l)); if (useDefaultZoom) { // Use default zoom level cameraPosition.zoom(CAMERA_DEFAULT_ZOOM); } else { // Use current zoom level cameraPosition.zoom(mMap.getCameraPosition().zoom); } if (animateToLocation) { // Smooth animation to position mMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition.build())); } else { // Abrupt change to position mMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition.build())); } } if (mController != null) { mController.onLocation(); } } public void zoomToRegion() { // If we have a region, then zoom to it. ObaRegion region = Application.get().getCurrentRegion(); if (region != null && mMap != null) { LatLngBounds b = MapHelpV2.getRegionBounds(region); // Use screen dimensions to avoid IllegalStateException (#581) int width = getResources().getDisplayMetrics().widthPixels; int height = getResources().getDisplayMetrics().heightPixels; int padding = 0; mMap.animateCamera((CameraUpdateFactory.newLatLngBounds(b, width, height, padding))); } } /** * Shows error messages related to stops, routes, and vehicles on the map, based on the * response * from the server * * @param response the response from the server, or null if the response object was null */ public static void showMapError(ObaResponse response) { Context context = Application.get().getApplicationContext(); int code; if (response != null) { code = response.getCode(); } else { // If we don't even have a response object, something went really wrong code = ObaApi.OBA_INTERNAL_ERROR; } if (UIUtils.canManageDialog(context)) { Toast.makeText(context, context.getString(UIUtils.getMapErrorString(context, code)), Toast.LENGTH_LONG).show(); } } // // MapView interactions // @Override public void setZoom(float zoomLevel) { if (mMap != null) { mMap.moveCamera(CameraUpdateFactory.zoomTo(zoomLevel)); } } @Override public Location getMapCenterAsLocation() { // If the center is the same as the last call to this method, pass back the same Location // object if (mMap != null) { LatLng center = mMap.getCameraPosition().target; if (mCenter == null || mCenter != center) { mCenter = center; mCenterLocation = MapHelpV2.makeLocation(mCenter); } } return mCenterLocation; } /** * Sets the map center to the given parameter * * @param location location to center on * @param animateToLocation true if the map should animate to the location, false if it should * snap to it * @param overlayExpanded true if the sliding panel is expanded, false if it is not */ @Override public void setMapCenter(Location location, boolean animateToLocation, boolean overlayExpanded) { if (mMap != null) { CameraPosition cp = mMap.getCameraPosition(); LatLng target = MapHelpV2.makeLatLng(location); LatLng offsetTarget; if (isRouteDisplayed() && overlayExpanded) { // Adjust camera target if the route header is currently displayed - map padding // doesn't get this quite right, as the header is slid up some and full padding doesn't apply double percentageOffset = 0.2; double bias = (getLongitudeSpanInDecDegrees() * percentageOffset) / 2; offsetTarget = new LatLng(target.latitude - bias, target.longitude); target = offsetTarget; } if (animateToLocation) { mMap.animateCamera(CameraUpdateFactory.newCameraPosition( new CameraPosition.Builder().target(target) .zoom(cp.zoom) .bearing(cp.bearing) .tilt(cp.tilt) .build() )); } else { mMap.moveCamera(CameraUpdateFactory.newCameraPosition( new CameraPosition.Builder().target(target) .zoom(cp.zoom) .bearing(cp.bearing) .tilt(cp.tilt) .build() )); } } } @Override public double getLatitudeSpanInDecDegrees() { VisibleRegion vr = mMap.getProjection().getVisibleRegion(); return Math.abs(vr.latLngBounds.northeast.latitude - vr.latLngBounds.southwest.latitude); } @Override public double getLongitudeSpanInDecDegrees() { VisibleRegion vr = mMap.getProjection().getVisibleRegion(); return Math.abs(vr.latLngBounds.northeast.longitude - vr.latLngBounds.southwest.longitude); } @Override public float getZoomLevelAsFloat() { return mMap.getCameraPosition().zoom; } @Override public void setRouteOverlay(int lineOverlayColor, ObaShape[] shapes, boolean clear) { if (mMap != null) { if (clear) { mLineOverlay.clear(); } PolylineOptions lineOptions; int totalPoints = 0; for (ObaShape s : shapes) { lineOptions = new PolylineOptions(); lineOptions.color(lineOverlayColor); for (Location l : s.getPoints()) { lineOptions.add(MapHelpV2.makeLatLng(l)); } // Add the line to the map, and keep a reference in the ArrayList mLineOverlay.add(mMap.addPolyline(lineOptions)); totalPoints += lineOptions.getPoints().size(); } Log.d(TAG, "Total points for route polylines = " + totalPoints); } } @Override public void setRouteOverlay(int lineOverlayColor, ObaShape[] shapes) { setRouteOverlay(lineOverlayColor, shapes, true); } /** * Updates markers for the provided routeIds from the status info from the given * ObaTripsForRouteResponse * * @param routeIds markers representing real-time positions for the provided routeIds will be * added to the map * @param response response that contains the real-time status info */ @Override public void updateVehicles(HashSet<String> routeIds, ObaTripsForRouteResponse response) { setupVehicleOverlay(); if (mVehicleOverlay != null) { mVehicleOverlay.updateVehicles(routeIds, response); } } @Override public void removeVehicleOverlay() { if (mVehicleOverlay != null) { mVehicleOverlay.clear(); } } @Override public void zoomToRoute() { if (mMap != null) { if (!mLineOverlay.isEmpty()) { LatLngBounds.Builder builder = new LatLngBounds.Builder(); for (Polyline p : mLineOverlay) { for (LatLng l : p.getPoints()) { builder.include(l); } } Activity a = getActivity(); if (a != null) { int padding = UIUtils.dpToPixels(a, DEFAULT_MAP_PADDING_DP); mMap.moveCamera( (CameraUpdateFactory.newLatLngBounds(builder.build(), padding))); } } else { Toast.makeText(getActivity(), getString(R.string.route_info_no_shape_data), Toast.LENGTH_SHORT).show(); } } } @Override public void zoomToItinerary() { if (mMap != null) { if (!mLineOverlay.isEmpty()) { LatLngBounds.Builder builder = new LatLngBounds.Builder(); for (Polyline p : mLineOverlay) { for (LatLng l : p.getPoints()) { builder.include(l); } } Activity a = getActivity(); if (a != null) { int padding = UIUtils.dpToPixels(a, DEFAULT_MAP_PADDING_DP); mMap.moveCamera( (CameraUpdateFactory.newLatLngBounds(builder.build(), getResources().getDisplayMetrics().widthPixels, getResources().getDisplayMetrics().heightPixels, padding))); } } } } /** * Zoom to include the current map bounds plus the location of the nearest vehicle * * @param routeIds markers representing real-time positions for the provided routeIds will be * checked for proximity to the location (all other routes are ignored) * @param response trips-for-route API response, which includes real-time vehicle locations in * status */ @Override public void zoomIncludeClosestVehicle(HashSet<String> routeIds, ObaTripsForRouteResponse response) { if (mMap == null) { return; } LatLng closestVehicleLocation = MapHelpV2 .getClosestVehicle(response, routeIds, getMapCenterAsLocation()); LatLngBounds visibleBounds = mMap.getProjection().getVisibleRegion().latLngBounds; if (closestVehicleLocation == null || visibleBounds.contains(closestVehicleLocation)) { // Closest vehicle is already in view or is null - don't change camera return; } // Zoom to include current map bounds and closest vehicle location LatLngBounds.Builder builder = new LatLngBounds.Builder(); builder.include(visibleBounds.northeast); builder.include(visibleBounds.southwest); builder.include(closestVehicleLocation); Activity a = getActivity(); if (a != null) { int padding = UIUtils.dpToPixels(a, DEFAULT_MAP_PADDING_DP); mMap.animateCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), padding)); } } @Override public void removeRouteOverlay() { for (Polyline p : mLineOverlay) { p.remove(); } mLineOverlay.clear(); } /** * Clears any stop markers from the map * * @param clearFocusedStop true to clear the currently focused stop, false to leave it on map */ @Override public void removeStopOverlay(boolean clearFocusedStop) { if (mStopOverlay != null) { mStopOverlay.clear(clearFocusedStop); } } @Override public boolean canWatchMapChanges() { // Android Map API v2 has an OnCameraChangeListener return true; } /** * Sets focus to a particular stop, or pass in null for the stop to clear the focus * * @param stop ObaStop to focus on, or null to clear the focus * @param routes a list of all route display names that serve this stop, or null to clear the * focus */ @Override public void setFocusStop(ObaStop stop, List<ObaRoute> routes) { // Make sure that the stop overlay has been successfully initialized before setting focus if (setupStopOverlay()) { mStopOverlay.setFocus(stop, routes); } } @Override public void onCameraChange(CameraPosition cameraPosition) { Log.d(TAG, "onCameraChange"); if (mController != null) { mController.notifyMapChanged(); } } // Maps V2 Location updates @Override public void activate(OnLocationChangedListener listener) { mListener = listener; } @Override public void deactivate() { mListener = null; } public void onLocationChanged(Location l) { if (mListener != null) { // Show real-time location on map mListener.onLocationChanged(l); } } @Override public void postInvalidate() { // Do nothing - calling `this.postInvalidate()` causes a StackOverflowError } // // VehicleOverlay.Controller // @Override public String getFocusedStopId() { return mFocusStopId; } // // Dialogs // public static class MapDialogFragment extends android.support.v4.app.DialogFragment { private static final String TAG = "MapDialogFragment"; int mDialogType; private static BaseMapFragment mMapFragment; private final static String DIALOG_TYPE_KEY = "dialog_type"; private static final int NOLOCATION_DIALOG = 103; private static final int OUTOFRANGE_DIALOG = 104; /** * Creates a new dialog of type NOLOCATION_DIALOG or OUTOFRANGE_DIALOG * * @param dialogType NOLOCATION_DIALOG to create a no location dialog, or OUTOFRANGE_DIALOG * to create an out of range dialog * @return a fragment to show the dialog */ static MapDialogFragment newInstance(int dialogType, BaseMapFragment fragment) { mMapFragment = fragment; MapDialogFragment f = new MapDialogFragment(); // Provide dialog type as an argument. Bundle args = new Bundle(); args.putInt(DIALOG_TYPE_KEY, dialogType); f.setArguments(args); f.setCancelable(false); return f; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { mDialogType = getArguments().getInt(DIALOG_TYPE_KEY); switch (mDialogType) { case NOLOCATION_DIALOG: return createNoLocationDialog(); case OUTOFRANGE_DIALOG: return createOutOfRangeDialog(); default: throw new IllegalArgumentException( "Invalid map dialog type - " + DIALOG_TYPE_KEY); } } private Dialog createOutOfRangeDialog() { Drawable icon = getResources().getDrawable(android.R.drawable.ic_dialog_map); DrawableCompat.setTint(icon, getResources().getColor(R.color.theme_primary)); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) .setTitle(R.string.main_outofrange_title) .setIcon(icon) .setCancelable(false) .setMessage(getString(R.string.main_outofrange, Application.get().getCurrentRegion() != null ? Application.get().getCurrentRegion().getName() : "" )) .setPositiveButton(R.string.main_outofrange_yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mMapFragment.zoomToRegion(); } } ) .setNegativeButton(R.string.main_outofrange_no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mMapFragment.mWarnOutOfRange = false; } } ); return builder.create(); } @SuppressWarnings("deprecation") private Dialog createNoLocationDialog() { View view = getActivity().getLayoutInflater().inflate(R.layout.no_location_dialog, null); CheckBox neverShowDialog = (CheckBox) view.findViewById(R.id.location_never_ask_again); neverShowDialog.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { // Save the preference PreferenceUtils.saveBoolean(getString(R.string.preference_key_never_show_location_dialog), isChecked); } }); Drawable icon = getResources().getDrawable(android.R.drawable.ic_dialog_map); DrawableCompat.setTint(icon, getResources().getColor(R.color.theme_primary)); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) .setTitle(R.string.main_nolocation_title) .setIcon(icon) .setCancelable(false) .setView(view) .setPositiveButton(R.string.rt_yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { startActivityForResult( new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS), REQUEST_NO_LOCATION); } } ) .setNegativeButton(R.string.rt_no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Ok, I suppose we can just try looking from where we // are. mMapFragment.mController.onLocation(); } } ); return builder.create(); } } }