/* * Copyright (C) 2012 - 2013 Niall 'Rivernile' Scott * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors or contributors be held liable for * any damages arising from the use of this software. * * The aforementioned copyright holder(s) hereby grant you a * non-transferrable right to use this software for any purpose (including * commercial applications), and to modify it and redistribute it, subject to * the following conditions: * * 1. This notice may not be removed or altered from any file it appears in. * * 2. Any modifications made to this software, except those defined in * clause 3 of this agreement, must be released under this license, and * the source code of any modifications must be made available on a * publically accessible (and locateable) website, or sent to the * original author of this software. * * 3. Software modifications that do not alter the functionality of the * software but are simply adaptations to a specific environment are * exempt from clause 2. */ package uk.org.rivernile.edinburghbustracker.android.fragments.general; import android.app.Activity; import android.app.SearchManager; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.provider.SearchRecentSuggestions; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; import android.util.TypedValue; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.Toast; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; 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.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.maps.model.Polyline; import com.google.android.gms.maps.model.PolylineOptions; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.regex.Matcher; import java.util.regex.Pattern; import uk.org.rivernile.edinburghbustracker.android.BusStopDatabase; import uk.org.rivernile.edinburghbustracker.android .MapSearchSuggestionsProvider; import uk.org.rivernile.edinburghbustracker.android.PreferencesActivity; import uk.org.rivernile.edinburghbustracker.android.R; import uk.org.rivernile.edinburghbustracker.android.fragments.dialogs .IndeterminateProgressDialogFragment; import uk.org.rivernile.edinburghbustracker.android.fragments.dialogs .MapTypeChooserDialogFragment; import uk.org.rivernile.edinburghbustracker.android.fragments.dialogs .ServicesChooserDialogFragment; import uk.org.rivernile.edinburghbustracker.android.maps.BusStopMarkerLoader; import uk.org.rivernile.edinburghbustracker.android.maps.GeoSearchLoader; import uk.org.rivernile.edinburghbustracker.android.maps.MapInfoWindow; import uk.org.rivernile.edinburghbustracker.android.maps.RouteLineLoader; /** * The BusStopMapFragment shows a Google Maps v2 MapView and depending on the * location of the camera, the zoom level and service filter, it shows bus stop * icons on the map. The user can tap on a bus stop icon to show the info * window (bubble). If the user taps on the info window, then the * BusStopDetailsFragment is shown. * * The user can also select the type of map these wish to see and search for bus * stops and places. * * @author Niall Scott */ public class BusStopMapFragment extends SupportMapFragment implements GoogleMap.OnCameraChangeListener, GoogleMap.OnMarkerClickListener, GoogleMap.OnInfoWindowClickListener, LoaderManager.LoaderCallbacks, ServicesChooserDialogFragment.Callbacks, MapTypeChooserDialogFragment.Callbacks, IndeterminateProgressDialogFragment.Callbacks { /** The stopCode argument. */ public static final String ARG_STOPCODE = "stopCode"; /** The latitude argument. */ public static final String ARG_LATITUDE = "latitude"; /** The longitude argument. */ public static final String ARG_LONGITUDE = "longitude"; /** The search argument. */ public static final String ARG_SEARCH = "searchTerm"; private static final String ARG_CHOSEN_SERVICES = "chosenServices"; /** The default latitude. */ public static final double DEFAULT_LAT = 55.948611; /** The default longitude. */ public static final double DEFAULT_LONG = -3.199811; /** The default zoom. */ public static final float DEFAULT_ZOOM = 11f; /** The default search zoom. */ public static final float DEFAULT_SEARCH_ZOOM = 16f; private static final Pattern STOP_CODE_PATTERN = Pattern.compile("(\\d{8})\\)$"); private static final Pattern STOP_CODE_SEARCH_PATTERN = Pattern.compile("^\\d{8}$"); private static final String LOADER_ARG_MIN_X = "minX"; private static final String LOADER_ARG_MIN_Y = "minY"; private static final String LOADER_ARG_MAX_X = "maxX"; private static final String LOADER_ARG_MAX_Y = "maxY"; private static final String LOADER_ARG_ZOOM = "zoom"; private static final String LOADER_ARG_FILTERED_SERVICES = "filteredServices"; private static final String LOADER_ARG_QUERY = "query"; private static final int LOADER_ID_BUS_STOPS = 0; private static final int LOADER_ID_GEO_SEARCH = 1; private static final int LOADER_ID_ROUTE_LINES = 2; private Callbacks callbacks; private BusStopDatabase bsd; private GoogleMap map; private SharedPreferences sp; private SearchManager searchMan; private final HashMap<String, Marker> busStopMarkers = new HashMap<String, Marker>(); private final HashMap<String, LinkedList<Polyline>> routeLines = new HashMap<String, LinkedList<Polyline>>(); private HashSet<Marker> geoSearchMarkers = new HashSet<Marker>(); private String searchedBusStop = null; private String[] services; private String[] chosenServices; private int actionBarHeight; /** * Create a new instance of the BusStopMapFragment, setting the initial * location to that of the stopCode provided. * * @param stopCode The stopCode to go to. * @return A new instance of this Fragment. */ public static BusStopMapFragment newInstance(final String stopCode) { final BusStopMapFragment f = new BusStopMapFragment(); final Bundle b = new Bundle(); b.putString(ARG_STOPCODE, stopCode); f.setArguments(b); return f; } /** * Create a new instance of the BusStopMapFragment, setting the initial * location specified by latitude and longitude. * * @param latitude The latitude to go to. * @param longitude The longitude to go to. * @return A new instance of this Fragment. */ public static BusStopMapFragment newInstance(final double latitude, final double longitude) { final BusStopMapFragment f = new BusStopMapFragment(); final Bundle b = new Bundle(); b.putDouble(ARG_LATITUDE, latitude); b.putDouble(ARG_LONGITUDE, longitude); f.setArguments(b); return f; } /** * Create a new instance of the BusStopMapFragment, specifying a search * term. The item will be searched as soon as the Fragment is ready. * * @param searchTerm The search term. * @return A new instance of this Fragment. */ public static BusStopMapFragment newInstanceWithSearch( final String searchTerm) { final BusStopMapFragment f = new BusStopMapFragment(); final Bundle b = new Bundle(); b.putString(ARG_SEARCH, searchTerm); f.setArguments(b); return f; } /** * {@inheritDoc} */ @Override public void onAttach(final Activity activity) { super.onAttach(activity); try { callbacks = (Callbacks) activity; } catch (ClassCastException e) { throw new IllegalStateException(activity.getClass().getName() + " does not implement " + Callbacks.class.getName()); } } /** * {@inheritDoc} */ @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Retain the instance to this Fragment. setRetainInstance(true); final Context context = getActivity(); bsd = BusStopDatabase.getInstance(context.getApplicationContext()); sp = context.getSharedPreferences(PreferencesActivity.PREF_FILE, 0); searchMan = (SearchManager) context .getSystemService(Context.SEARCH_SERVICE); services = bsd.getBusServiceList(); if (savedInstanceState != null) { chosenServices = savedInstanceState .getStringArray(ARG_CHOSEN_SERVICES); } // Get the height of the ActionBar from the assigned attribute in the // appcompat project theme. final TypedValue value = new TypedValue(); getActivity().getTheme().resolveAttribute( android.support.v7.appcompat.R.attr.actionBarSize, value, true); actionBarHeight = getResources() .getDimensionPixelSize(value.resourceId); // This Fragment shows an options menu. setHasOptionsMenu(true); } /** * {@inheritDoc} */ @Override public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if(map == null) { map = getMap(); if(map != null) { getActivity().supportInvalidateOptionsMenu(); final UiSettings uiSettings = map.getUiSettings(); uiSettings.setRotateGesturesEnabled(false); uiSettings.setCompassEnabled(false); uiSettings.setMyLocationButtonEnabled(true); map.setInfoWindowAdapter(new MapInfoWindow(getActivity())); map.setOnCameraChangeListener(this); map.setOnMarkerClickListener(this); map.setOnInfoWindowClickListener(this); map.setMapType(sp.getInt( PreferencesActivity.PREF_MAP_LAST_MAP_TYPE, GoogleMap.MAP_TYPE_NORMAL)); map.setPadding(0, actionBarHeight, 0, 0); moveCameraToInitialLocation(); refreshBusStops(null); // Check to see if a search is to be done. final Bundle args = getArguments(); if(args != null && args.containsKey(ARG_SEARCH)) { onSearch(args.getString(ARG_SEARCH)); args.remove(ARG_SEARCH); } } } } /** * {@inheritDoc} */ @Override public void onResume() { super.onResume(); if(map != null) { final SharedPreferences sharedPrefs = getActivity() .getSharedPreferences(PreferencesActivity.PREF_FILE, 0); map.setMyLocationEnabled(sharedPrefs .getBoolean(PreferencesActivity.PREF_AUTO_LOCATION, true)); map.getUiSettings().setZoomControlsEnabled(sharedPrefs .getBoolean(PreferencesActivity.PREF_ZOOM_BUTTONS, true)); } } /** * {@inheritDoc} */ @Override public void onPause() { super.onPause(); if(map != null) { // Save the camera location to SharedPreferences, so the user is // shown this location when they load the map again. final SharedPreferences.Editor edit = sp.edit(); final CameraPosition position = map.getCameraPosition(); final LatLng latLng = position.target; edit.putString(PreferencesActivity.PREF_MAP_LAST_LATITUDE, String.valueOf(latLng.latitude)); edit.putString(PreferencesActivity.PREF_MAP_LAST_LONGITUDE, String.valueOf(latLng.longitude)); edit.putFloat(PreferencesActivity.PREF_MAP_LAST_ZOOM, position.zoom); edit.putInt(PreferencesActivity.PREF_MAP_LAST_MAP_TYPE, map.getMapType()); edit.commit(); } } /** * {@inheritDoc} */ @Override public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); outState.putStringArray(ARG_CHOSEN_SERVICES, chosenServices); } /** * {@inheritDoc} */ @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { inflater.inflate(R.menu.busstopmap_option_menu, menu); final MenuItem searchItem = menu .findItem(R.id.busstopmap_option_menu_search); final SearchView searchView = (SearchView) MenuItemCompat .getActionView(searchItem); searchView.setSearchableInfo(searchMan .getSearchableInfo(getActivity().getComponentName())); } /** * {@inheritDoc} */ @Override public void onPrepareOptionsMenu(final Menu menu) { super.onPrepareOptionsMenu(menu); MenuItem item = menu.findItem(R.id.busstopmap_option_menu_trafficview); if(map != null && map.isTrafficEnabled()) { item.setTitle(R.string.map_menu_mapoverlay_trafficviewoff); } else { item.setTitle(R.string.map_menu_mapoverlay_trafficviewon); } item.setEnabled(map != null); item = menu.findItem(R.id.busstopmap_option_menu_services); item.setEnabled(services != null && services.length > 0); } /** * {@inheritDoc} */ @Override public boolean onOptionsItemSelected(final MenuItem item) { switch(item.getItemId()) { case R.id.busstopmap_option_menu_services: callbacks.onShowServicesChooser(services, chosenServices, getString(R.string .busstopmapfragment_service_chooser_title)); return true; case R.id.busstopmap_option_menu_maptype: callbacks.onShowMapTypeSelection(); return true; case R.id.busstopmap_option_menu_trafficview: // Toggle the traffic view. if(map != null) { map.setTrafficEnabled(!map.isTrafficEnabled()); getActivity().supportInvalidateOptionsMenu(); } return true; default: return super.onOptionsItemSelected(item); } } /** * {@inheritDoc} */ @Override public void onCameraChange(final CameraPosition position) { // If the camera has changed, force a refresh of the bus stop markers. refreshBusStops(position); } /** * {@inheritDoc} */ @Override public boolean onMarkerClick(final Marker marker) { final String snippet = marker.getSnippet(); if(busStopMarkers.containsValue(marker) && (snippet == null || snippet.length() == 0)) { final Matcher matcher = STOP_CODE_PATTERN .matcher(marker.getTitle()); if(matcher.find()) { final String stopCode = matcher.group(1); marker.setSnippet(bsd.getBusServicesForStopAsString(stopCode)); } } return false; } /** * {@inheritDoc} */ @Override public void onInfoWindowClick(final Marker marker) { if(busStopMarkers.containsValue(marker)) { final Matcher matcher = STOP_CODE_PATTERN.matcher( marker.getTitle()); if(matcher.find()) { callbacks.onShowBusStopDetails(matcher.group(1)); } } } /** * {@inheritDoc} */ @Override public Loader onCreateLoader(final int i, final Bundle bundle) { switch(i) { case LOADER_ID_BUS_STOPS: if(bundle.containsKey(LOADER_ARG_FILTERED_SERVICES)) { return new BusStopMarkerLoader( getActivity(), bundle.getDouble(LOADER_ARG_MIN_X), bundle.getDouble(LOADER_ARG_MIN_Y), bundle.getDouble(LOADER_ARG_MAX_X), bundle.getDouble(LOADER_ARG_MAX_Y), bundle.getFloat(LOADER_ARG_ZOOM), bundle.getStringArray(LOADER_ARG_FILTERED_SERVICES) ); } else { return new BusStopMarkerLoader( getActivity(), bundle.getDouble(LOADER_ARG_MIN_X), bundle.getDouble(LOADER_ARG_MIN_Y), bundle.getDouble(LOADER_ARG_MAX_X), bundle.getDouble(LOADER_ARG_MAX_Y), bundle.getFloat(LOADER_ARG_ZOOM) ); } case LOADER_ID_GEO_SEARCH: String query = bundle.getString(LOADER_ARG_QUERY); // Make sure the query arg is not null. if(query == null) { query = ""; } return new GeoSearchLoader(getActivity(), query); case LOADER_ID_ROUTE_LINES: return new RouteLineLoader(getActivity(), bundle.getStringArray(LOADER_ARG_FILTERED_SERVICES)); default: return null; } } /** * {@inheritDoc} */ @Override public void onLoadFinished(final Loader loader, final Object d) { if (isAdded()) { switch (loader.getId()) { case LOADER_ID_BUS_STOPS: addBusStopMarkers((HashMap<String, MarkerOptions>)d); break; case LOADER_ID_GEO_SEARCH: addGeoSearchResults((HashSet<MarkerOptions>)d); break; case LOADER_ID_ROUTE_LINES: addRouteLines((HashMap<String, LinkedList<PolylineOptions>>)d); break; default: break; } } } /** * {@inheritDoc} */ @Override public void onLoaderReset(final Loader loader) { // Nothing to do. } /** * {@inheritDoc} */ @Override public void onServicesChosen(final String[] chosenServices) { this.chosenServices = chosenServices; // If the user has chosen services in the services filter, force a // refresh of the marker icons. refreshBusStops(null); final LinkedList<String> tempList = new LinkedList<String>(); boolean found; // Loop through the existing route lines. If a service doesn't exist in // the chosen services list, add it to the to-be-removed list. for(String key : routeLines.keySet()) { found = false; for(String fs : chosenServices) { if(key.equals(fs)) { found = true; break; } } if(!found) { tempList.add(key); } } LinkedList<Polyline> polyLines; // Loop through the to-be-removed list and remove the Polylines and the // entry from the routeLines HashMap. for(String toRemove : tempList) { polyLines = routeLines.get(toRemove); routeLines.remove(toRemove); for(Polyline pl : polyLines) { pl.remove(); } } // The tempList is going to be reused, so clear it out. tempList.clear(); // Loop through the filteredServices array. If the element does not // appear in the existing route lines, then add it to the to-be-added // list. for(String fs : chosenServices) { if(!routeLines.containsKey(fs)) { tempList.add(fs); } } final int size = tempList.size(); // Execute the load if there are routes to be loaded. if(size > 0) { final String[] servicesToLoad = new String[size]; tempList.toArray(servicesToLoad); final Bundle b = new Bundle(); b.putStringArray(LOADER_ARG_FILTERED_SERVICES, servicesToLoad); getLoaderManager().restartLoader(LOADER_ID_ROUTE_LINES, b, this); } } /** * {@inheritDoc} */ @Override public void onMapTypeChosen(final int mapType) { // When the user selects a new map type, change the map type. if(map != null) { map.setMapType(mapType); } } /** * {@inheritDoc} */ @Override public void onProgressCancel() { // If the user cancels the search progress dialog, then cancel the // search loader. getLoaderManager().destroyLoader(LOADER_ID_GEO_SEARCH); } /** * This is called when the back button is pressed. It is called by the * underlying Activity. * * @return true if the back event was handled here, false if not. */ public boolean onBackPressed() { if(!geoSearchMarkers.isEmpty()) { for(Marker m : geoSearchMarkers) { if(m.isInfoWindowShown()) { m.hideInfoWindow(); return true; } } for(Marker m : geoSearchMarkers) { m.remove(); } geoSearchMarkers.clear(); return true; } // Loop through all the bus stop markers, and if any have an info // window shown, hide it then prevent the default back button // behaviour. for(Marker m : busStopMarkers.values()) { if(m.isInfoWindowShown()) { m.hideInfoWindow(); return true; } } return false; } /** * This method is called by the underlying Activity when a search has been * initiated. * * @param searchTerm What to search for. */ public void onSearch(final String searchTerm) { if(map == null) { return; } final Matcher m = STOP_CODE_SEARCH_PATTERN.matcher(searchTerm); if(m.matches()) { // If the searchTerm is a stop code, then move the camera to the bus // stop. moveCameraToBusStop(searchTerm); } else { // If it's not a stop code, then do a geo search. final Bundle b = new Bundle(); b.putString(LOADER_ARG_QUERY, searchTerm); // Save the search term as a search suggestion. final SearchRecentSuggestions suggestions = new SearchRecentSuggestions(getActivity(), MapSearchSuggestionsProvider.AUTHORITY, MapSearchSuggestionsProvider.MODE); suggestions.saveRecentQuery(searchTerm, null); // Start the search loader. getLoaderManager().restartLoader(LOADER_ID_GEO_SEARCH, b, this); callbacks.onShowSearchProgress( getString(R.string.busstopmapfragment_progress_message, searchTerm)); } } /** * Move the camera to a given stopCode, and show the info window for that * stopCode when the camera gets there. * * @param stopCode The stopCode to move to. */ public void moveCameraToBusStop(final String stopCode) { if(stopCode == null || stopCode.length() == 0) { return; } searchedBusStop = stopCode; moveCameraToLocation(bsd.getLatLngForStopCode(stopCode), DEFAULT_SEARCH_ZOOM, true); } /** * Move the camera to a given LatLng location. * * @param location Where to move the camera to. * @param zoomLevel The zoom level of the camera. * @param animate Whether the transition should be animated or not. */ public void moveCameraToLocation(final LatLng location, final float zoomLevel, final boolean animate) { if(location == null) { return; } final CameraUpdate update = CameraUpdateFactory.newLatLngZoom(location, zoomLevel); if(animate) { map.animateCamera(update); } else { map.moveCamera(update); } } /** * Refresh the bus stop marker icons on the map. This may be because the * camera has moved, a configuration change has happened or the user has * selected services to filter by. * * @param position If a CameraPosition is available, send it in so that it * doesn't need to be looked up again. If it's not available, use null. */ private void refreshBusStops(CameraPosition position) { if(map == null || !isAdded()) { return; } // Populate the CameraPosition if it wasn't given. if(position == null) { position = map.getCameraPosition(); } // Get the visible bounds. final LatLngBounds lastVisibleBounds = map.getProjection().getVisibleRegion().latLngBounds; final Bundle b = new Bundle(); // Populate the Bundle of arguments for the bus stops Loader. b.putDouble(LOADER_ARG_MIN_X, lastVisibleBounds.southwest.latitude); b.putDouble(LOADER_ARG_MIN_Y, lastVisibleBounds.southwest.longitude); b.putDouble(LOADER_ARG_MAX_X, lastVisibleBounds.northeast.latitude); b.putDouble(LOADER_ARG_MAX_Y, lastVisibleBounds.northeast.longitude); b.putFloat(LOADER_ARG_ZOOM, position.zoom); // If there are chosen services, then set the filtered services // argument. if(chosenServices != null && chosenServices.length > 0) { b.putStringArray(LOADER_ARG_FILTERED_SERVICES, chosenServices); } // Start the bus stops Loader. getLoaderManager().restartLoader(LOADER_ID_BUS_STOPS, b, this); } /** * This method is called when the bus stops Loader has finished loading bus * stops and has data ready to be populated on the map. * * @param result The data to be populated on the map. */ private void addBusStopMarkers( final HashMap<String, MarkerOptions> result) { if(map == null) { return; } // Get an array of the stopCodes that are currently on the map. This is // given to us as an array of Objects, which cannot be cast to an array // of Strings. final Object[] currentStops = busStopMarkers.keySet() .toArray(); Marker marker; for(Object existingStop : currentStops) { marker = busStopMarkers.get((String)existingStop); // If the new data does not contain the given stopCode, and the // marker for that bus stop doesn't have an info window shown, then // remove it. if(!result.containsKey((String)existingStop) && !marker.isInfoWindowShown()) { marker.remove(); busStopMarkers.remove((String)existingStop); } else { // Otherwise, remove the bus stop from the new data as it is // already populated on the map and doesn't need to be // re-populated. This is a performance enhancement. result.remove((String)existingStop); } } // Loop through all the new bus stops, and add them to the map. Bus // stops common to the existing collection and the new collection will // not be touched. for(String newStop : result.keySet()) { busStopMarkers.put(newStop, map.addMarker(result.get(newStop))); } // If map has been moved to this location because the user searched for // a specific bus stop... if(searchedBusStop != null) { marker = busStopMarkers.get(searchedBusStop); // If the marker has been found... if(marker != null) { // Get the snippet text for the marker and if it does not exist, // populate it with the bus services list. final String snippet = marker.getSnippet(); if(snippet == null || snippet.length() == 0) { marker.setSnippet(bsd.getBusServicesForStopAsString( searchedBusStop)); } // Show the info window of the marker to highlight it. marker.showInfoWindow(); // Set this to null to make sure the stop isn't highlighted // again, until the user initiates another search. searchedBusStop = null; } } } /** * This method is called when the search Loader has finished loading and * data is to be populated on the map. * * @param result The data to be populated on the map. */ private void addGeoSearchResults(final HashSet<MarkerOptions> result) { if (!isAdded()) { return; } // If there is a progress Dialog, get rid of it. callbacks.onDismissSearchProgress(); if(map == null) { return; } // Remove all of the existing search markers from the map. for(Marker m : geoSearchMarkers) { m.remove(); } // ...and because they've been cleared from the map, remove them all // from the collection. geoSearchMarkers.clear(); // If there are no results, show a Toast notification to the user. if(result == null || result.isEmpty()) { Toast.makeText(getActivity(), R.string.busstopmapfragment_nosearchresults, Toast.LENGTH_LONG).show(); return; } Marker marker; boolean isFirst = true; for(MarkerOptions mo : result) { // Add the new marker to the map. marker = map.addMarker(mo); // Make sure the item does not already exist in the marker list. If // it does, remove it from the map again. if(!geoSearchMarkers.add(marker)) { marker.remove(); } else if(isFirst) { // If it's the first icon to be added, move the camera to that // bus stop marker. isFirst = false; moveCameraToLocation(marker.getPosition(), DEFAULT_SEARCH_ZOOM, true); marker.showInfoWindow(); } } } /** * Add route lines to the Map. This is called when the route lines loader * has finished loading the route lines. * * @param result A HashMap, mapping the service name to a LinkedList of * PolylineOptions objects. This is a LinkedList because a service may have * more than one Polyline. */ private void addRouteLines( final HashMap<String, LinkedList<PolylineOptions>> result) { if(map == null) { return; } LinkedList<PolylineOptions> polyLineOptions; LinkedList<Polyline> newPolyLines; // Loop through all services in the HashMap. for(String service : result.keySet()) { polyLineOptions = result.get(service); // Create the LinkedList that the Polylines will be stored in. newPolyLines = new LinkedList<Polyline>(); // Add the LinkedList to the routeLines HashMap. routeLines.put(service, newPolyLines); // Loop through all the PolylineOptions for this service, and add // them to the map and the Polyline LinkedList. for(PolylineOptions plo : polyLineOptions) { newPolyLines.add(map.addPolyline(plo)); } } } /** * Move the camera to the initial location. The initial location is * determined by the following order; * * - If the args contains a stopCode, go there. * - If the args contains a latitude AND a longitude, go there. * - If the SharedPreferences have mappings for a previous location, then * go there. * - Otherwise, go to the default map location, as defined by * {@link #DEFAULT_LAT} and {@link #DEFAULT_LONG) at * {@link #DEFAULT_ZOOM}. */ private void moveCameraToInitialLocation() { final Bundle args = getArguments(); if(args != null && args.containsKey(ARG_STOPCODE)) { moveCameraToBusStop(args.getString(ARG_STOPCODE)); args.remove(ARG_STOPCODE); } else if(args != null && args.containsKey(ARG_LATITUDE) && args.containsKey(ARG_LONGITUDE)) { moveCameraToLocation(new LatLng(args.getDouble(ARG_LATITUDE), args.getDouble(ARG_LONGITUDE)), DEFAULT_SEARCH_ZOOM, false); args.remove(ARG_LATITUDE); args.remove(ARG_LONGITUDE); } else if(map != null) { // The Lat/Lons have to be treated as Strings because // SharedPreferences has no support for doubles. final String latitude = sp.getString( PreferencesActivity.PREF_MAP_LAST_LATITUDE, String.valueOf(DEFAULT_LAT)); final String longitude = sp.getString( PreferencesActivity.PREF_MAP_LAST_LONGITUDE, String.valueOf(DEFAULT_LONG)); final float zoom = sp.getFloat( PreferencesActivity.PREF_MAP_LAST_ZOOM, DEFAULT_ZOOM); try { moveCameraToLocation(new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude)), zoom, false); } catch(NumberFormatException e) { moveCameraToLocation(new LatLng(DEFAULT_LAT, DEFAULT_LONG), DEFAULT_ZOOM, false); } } } /** * Any Activities which host this Fragment must implement this interface to * handle navigation events. */ public static interface Callbacks { /** * This is called when the user wishes to select their preferred map * type. */ public void onShowMapTypeSelection(); /** * This is called when the user wishes to select services, for example, * for filtering. * * @param services The services to choose from. * @param selectedServices Any services that should be selected by * default. * @param title A title to show on the chooser. */ public void onShowServicesChooser(String[] services, String[] selectedServices, String title); /** * This is called when the user has initiated a search and progress * should be shown. * * @param message The message to show the user. */ public void onShowSearchProgress(String message); /** * This is called when the search progress should be dimissed. */ public void onDismissSearchProgress(); /** * This is called when the user wants to see details about a bus stop. * * @param stopCode The bus stop code the user wants to see details for. */ public void onShowBusStopDetails(String stopCode); } }