Files
OpenTracks/MyTracks/src/com/google/android/apps/mytracks/fragments/MyTracksMapFragment.java
T

769 lines
25 KiB
Java

/*
* Copyright 2008 Google Inc.
*
* 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 com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.MapOverlay;
import com.google.android.apps.mytracks.MarkerDetailActivity;
import com.google.android.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.MyTracksLocationManager;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.GoogleLocationUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.location.LocationListener;
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.GoogleMap.OnCameraChangeListener;
import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener;
import com.google.android.gms.maps.LocationSource;
import com.google.android.gms.maps.LocationSource.OnLocationChangedListener;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.os.Looper;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.EnumSet;
/**
* A fragment to display map to the user.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class MyTracksMapFragment extends SupportMapFragment implements TrackDataListener {
public static final String MAP_FRAGMENT_TAG = "mapFragment";
private static final String CURRENT_LOCATION_KEY = "current_location_key";
private static final String
KEEP_CURRENT_LOCATION_VISIBLE_KEY = "keep_current_location_visible_key";
private static final String ZOOM_TO_CURRENT_LOCATION_KEY = "zoom_to_current_location_key";
private static final float DEFAULT_ZOOM_LEVEL = 18f;
// Google's latitude and longitude
private static final double DEFAULT_LATITUDE = 37.423;
private static final double DEFAULT_LONGITUDE = -122.084;
private static final int MAP_VIEW_PADDING = 32;
private TrackDataHub trackDataHub;
// Current location
private Location currentLocation;
private Location lastTrackPoint;
private int minRequiredAccuracy = PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT;
/**
* True to continue keeping the current location visible on the screen.
* <p>
* Set to true when <br>
* 1. user clicks on the my location button <br>
* 2. first location during a recording <br>
* Set to false when <br>
* 1. showing a marker <br>
* 2. user manually zooms/pans
*/
private boolean keepCurrentLocationVisible;
/**
* True to zoom/center the current location when it is available.
* <p>
* Set to true when <br>
* 1. user clicks on the my location button <br>
* 2. first location during a recording <br>
* Set to false when <br>
* 1. showing a marker <br>
* 2. user manually zooms/pans <br>
* 3. after zooming to the current location <br>
* <p>
* The last one is to support the use case of zooming only once. After zoom,
* set it to false. E.g., only zoom to the first location during a recording.
*/
private boolean zoomToCurrentLocation;
private OnLocationChangedListener onLocationChangedListener;
// For showing a marker
private long markerTrackId = -1L;
private long markerId = -1L;
// Current track
private Track currentTrack;
// Current paths
private ArrayList<Polyline> paths = new ArrayList<Polyline>();
boolean reloadPaths = true;
// UI elements
private GoogleMap googleMap;
private MapOverlay mapOverlay;
private View mapView;
private ImageButton myLocationImageButton;
private TextView messageTextView;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setHasOptionsMenu(true);
mapOverlay = new MapOverlay(getActivity());
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mapView = super.onCreateView(inflater, container, savedInstanceState);
View layout = inflater.inflate(R.layout.map, container, false);
RelativeLayout mapContainer = (RelativeLayout) layout.findViewById(R.id.map_container);
mapContainer.addView(mapView, 0);
myLocationImageButton = (ImageButton) layout.findViewById(R.id.map_my_location);
myLocationImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final MyTracksLocationManager myTracksLocationManager = new MyTracksLocationManager(
getActivity(), Looper.myLooper(), true);
if (!myTracksLocationManager.isAllowed()) {
String setting = getString(
GoogleLocationUtils.isAvailable(getActivity()) ? R.string.gps_google_location_settings
: R.string.gps_location_access);
Toast.makeText(
getActivity(), getString(R.string.my_location_no_gps, setting), Toast.LENGTH_LONG)
.show();
myTracksLocationManager.close();
} else {
myTracksLocationManager.requestLastLocation(new LocationListener() {
@Override
public void onLocationChanged(Location location) {
myTracksLocationManager.close();
keepCurrentLocationVisible = true;
zoomToCurrentLocation = true;
setCurrentLocation(location);
updateCurrentLocation();
}
});
}
}
});
messageTextView = (TextView) layout.findViewById(R.id.map_message);
/*
* At this point, after super.onCreateView, getMap will not return null and
* we can initialize googleMap. However, onCreateView can be called multiple
* times, e.g., when the user switches tabs. With
* GoogleMapOptions.useViewLifecycleInFragment == false, googleMap lifecycle
* is tied to the fragment lifecycle and the same googleMap object is
* returned in getMap. Thus we only need to initialize googleMap once, when
* it is null.
*/
if (googleMap == null) {
googleMap = getMap();
googleMap.setMyLocationEnabled(true);
/*
* My Tracks needs to handle the onClick event when the my location button
* is clicked. Currently, the API doesn't allow handling onClick event,
* thus hiding the default my location button and providing our own.
*/
googleMap.getUiSettings().setMyLocationButtonEnabled(false);
googleMap.setIndoorEnabled(true);
googleMap.setOnMarkerClickListener(new OnMarkerClickListener() {
@Override
public boolean onMarkerClick(Marker marker) {
if (isResumed()) {
String title = marker.getTitle();
if (title != null && title.length() > 0) {
long id = Long.valueOf(title);
Context context = getActivity();
Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
context.startActivity(intent);
}
}
return true;
}
});
googleMap.setLocationSource(new LocationSource() {
@Override
public void activate(OnLocationChangedListener listener) {
onLocationChangedListener = listener;
}
@Override
public void deactivate() {
onLocationChangedListener = null;
}
});
googleMap.setOnCameraChangeListener(new OnCameraChangeListener() {
@Override
public void onCameraChange(CameraPosition cameraPosition) {
if (isResumed() && keepCurrentLocationVisible && currentLocation != null
&& !isLocationVisible(currentLocation)) {
keepCurrentLocationVisible = false;
zoomToCurrentLocation = false;
}
}
});
googleMap.moveCamera(
CameraUpdateFactory.newLatLngZoom(getDefaultLatLng(), googleMap.getMinZoomLevel()));
}
return layout;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
keepCurrentLocationVisible = savedInstanceState.getBoolean(
KEEP_CURRENT_LOCATION_VISIBLE_KEY, false);
zoomToCurrentLocation = savedInstanceState.getBoolean(ZOOM_TO_CURRENT_LOCATION_KEY, false);
Location location = (Location) savedInstanceState.getParcelable(CURRENT_LOCATION_KEY);
setCurrentLocation(location);
}
}
@Override
public void onResume() {
super.onResume();
if (googleMap != null) {
int mapType = PreferencesUtils.getInt(
getActivity(), R.string.map_type_key, PreferencesUtils.MAP_TYPE_DEFAUlT);
googleMap.setMapType(mapType);
ApiAdapterFactory.getApiAdapter().invalidMenu(getActivity());
}
resumeTrackDataHub();
updateCurrentLocation();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (currentLocation != null) {
/*
* currentLocation is a MyTracksLocation object, which cannot be
* unmarshalled. Thus creating a Location object before placing it in the
* bundle.
*/
outState.putParcelable(CURRENT_LOCATION_KEY, new Location(currentLocation));
}
outState.putBoolean(KEEP_CURRENT_LOCATION_VISIBLE_KEY, keepCurrentLocationVisible);
outState.putBoolean(ZOOM_TO_CURRENT_LOCATION_KEY, zoomToCurrentLocation);
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
}
/**
* Shows the marker on the map.
*
* @param trackId the track id
* @param id the marker id
*/
public void showMarker(long trackId, long id) {
/*
* Synchronize to prevent race condition in changing markerTrackId and
* markerId variables.
*/
synchronized (this) {
if (currentTrack != null && currentTrack.getId() == trackId) {
showMarker(id);
markerTrackId = -1L;
markerId = -1L;
return;
}
markerTrackId = trackId;
markerId = id;
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflator) {
menuInflator.inflate(R.menu.map, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
if (googleMap != null) {
int id;
switch (googleMap.getMapType()) {
case GoogleMap.MAP_TYPE_NORMAL:
id = R.id.menu_map;
break;
case GoogleMap.MAP_TYPE_SATELLITE:
id = R.id.menu_satellite;
break;
case GoogleMap.MAP_TYPE_HYBRID:
id = R.id.menu_satellite_with_streets;
break;
case GoogleMap.MAP_TYPE_TERRAIN:
id = R.id.menu_terrain;
break;
default:
id = R.id.menu_map;
}
MenuItem menuItem = menu.findItem(id);
if (menuItem != null) {
menuItem.setChecked(true);
}
}
super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
int type = GoogleMap.MAP_TYPE_NORMAL;
switch (menuItem.getItemId()) {
case R.id.menu_map:
type = GoogleMap.MAP_TYPE_NORMAL;
break;
case R.id.menu_satellite:
type = GoogleMap.MAP_TYPE_SATELLITE;
break;
case R.id.menu_satellite_with_streets:
type = GoogleMap.MAP_TYPE_HYBRID;
break;
case R.id.menu_terrain:
type = GoogleMap.MAP_TYPE_TERRAIN;
break;
default:
return super.onOptionsItemSelected(menuItem);
}
if (googleMap != null) {
googleMap.setMapType(type);
menuItem.setChecked(true);
PreferencesUtils.setInt(getActivity(), R.string.map_type_key, type);
}
return true;
}
@Override
public void onSelectedTrackChanged(final Track track) {
if (isResumed()) {
currentTrack = track;
boolean hasTrack = track != null;
if (hasTrack) {
mapOverlay.setShowEndMarker(!isSelectedTrackRecording());
synchronized (this) {
/*
* Synchronize to prevent race condition in changing markerTrackId and
* markerId variables.
*/
if (track.getId() == markerTrackId) {
// Show the marker
showMarker(markerId);
markerTrackId = -1L;
markerId = -1L;
} else {
// Show the track
showTrack();
}
}
}
}
}
@Override
public void onTrackUpdated(Track track) {
// We don't care.
}
@Override
public void clearTrackPoints() {
lastTrackPoint = null;
if (isResumed()) {
mapOverlay.clearPoints();
reloadPaths = true;
}
}
@Override
public void onSampledInTrackPoint(final Location location) {
lastTrackPoint = location;
if (isResumed()) {
mapOverlay.addLocation(location);
}
}
@Override
public void onSampledOutTrackPoint(Location location) {
lastTrackPoint = location;
}
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
mapOverlay.addSegmentSplit();
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
if (isResumed() && googleMap != null) {
mapOverlay.update(googleMap, paths, reloadPaths);
reloadPaths = false;
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
lastTrackPoint = null;
}
if (lastTrackPoint != null) {
setCurrentLocation(lastTrackPoint);
updateCurrentLocation();
setWarningMessage(true);
}
}
}
});
}
}
@Override
public void clearWaypoints() {
if (isResumed()) {
mapOverlay.clearWaypoints();
}
}
@Override
public void onNewWaypoint(Waypoint waypoint) {
if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
mapOverlay.addWaypoint(waypoint);
}
}
@Override
public void onNewWaypointsDone() {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
if (isResumed() && googleMap != null) {
mapOverlay.update(googleMap, paths, true);
}
}
});
}
}
@Override
public boolean onMetricUnitsChanged(boolean metric) {
// We don't care.
return false;
}
@Override
public boolean onReportSpeedChanged(boolean reportSpeed) {
// We don't care.
return false;
}
@Override
public boolean onMinRequiredAccuracy(int newValue) {
minRequiredAccuracy = newValue;
return false;
}
@Override
public boolean onMinRecordingDistanceChanged(int minRecordingDistance) {
// We don't care.
return false;
}
/**
* Resumes the trackDataHub. Needs to be synchronized because the trackDataHub
* can be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.SELECTED_TRACK,
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE));
MyTracksLocationManager myTracksLocationManager = new MyTracksLocationManager(
getActivity(), Looper.myLooper(), false);
boolean isGpsProviderEnabled = myTracksLocationManager.isGpsProviderEnabled();
if (googleMap != null) {
googleMap.setMyLocationEnabled(isGpsProviderEnabled);
}
setWarningMessage(isGpsProviderEnabled);
myTracksLocationManager.close();
}
/**
* Pauses the trackDataHub. Needs to be synchronized because the trackDataHub
* can be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.unregisterTrackDataListener(this);
}
trackDataHub = null;
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because the trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Returns true if the selected track is paused. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackPaused() {
return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
}
/**
* Sets the current location.
*
* @param location the location
*/
private void setCurrentLocation(Location location) {
// If recording, zoom to the first location
if (isSelectedTrackRecording() && currentLocation == null && location != null) {
keepCurrentLocationVisible = true;
zoomToCurrentLocation = true;
}
currentLocation = location;
}
/**
* Updates the current location and zoom to it if necessary.
*/
private void updateCurrentLocation() {
getActivity().runOnUiThread(new Runnable() {
public void run() {
if (!isResumed() || googleMap == null || onLocationChangedListener == null
|| currentLocation == null) {
return;
}
onLocationChangedListener.onLocationChanged(currentLocation);
if (zoomToCurrentLocation
|| (keepCurrentLocationVisible && !isLocationVisible(currentLocation))) {
LatLng latLng = new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude());
googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, DEFAULT_ZOOM_LEVEL));
zoomToCurrentLocation = false;
}
};
});
}
/**
* Sets the camera over a track.
*/
private void showTrack() {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isResumed() || googleMap == null || currentTrack == null
|| currentTrack.getNumberOfPoints() < 2) {
return;
}
/**
* Check that mapView is valid.
*/
if (mapView == null || mapView.getWidth() == 0 || mapView.getHeight() == 0) {
return;
}
TripStatistics tripStatistics = currentTrack.getTripStatistics();
int latitudeSpanE6 = tripStatistics.getTop() - tripStatistics.getBottom();
int longitudeSpanE6 = tripStatistics.getRight() - tripStatistics.getLeft();
if (latitudeSpanE6 > 0 && latitudeSpanE6 < 180E6 && longitudeSpanE6 > 0
&& longitudeSpanE6 < 360E6) {
LatLng southWest = new LatLng(
tripStatistics.getBottomDegrees(), tripStatistics.getLeftDegrees());
LatLng northEast = new LatLng(
tripStatistics.getTopDegrees(), tripStatistics.getRightDegrees());
LatLngBounds bounds = LatLngBounds.builder()
.include(southWest).include(northEast).build();
/**
* Note cannot call CameraUpdate.newLatLngBounds(LatLngBounds bounds,
* int padding) if the map view has not undergone layout. Thus calling
* CameraUpdate.newLatLngBounds(LatLngBounds bounds, int width, int
* height, int padding) after making sure that mapView is valid in the
* above code.
*/
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngBounds(
bounds, mapView.getWidth(), mapView.getHeight(), MAP_VIEW_PADDING);
googleMap.moveCamera(cameraUpdate);
}
}
});
}
/**
* Sets the camera over a marker.
*
* @param id the marker id
*/
private void showMarker(final long id) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isResumed() || googleMap == null) {
return;
}
MyTracksProviderUtils MyTracksProviderUtils = Factory.get(getActivity());
Waypoint waypoint = MyTracksProviderUtils.getWaypoint(id);
if (waypoint != null && waypoint.getLocation() != null) {
Location location = waypoint.getLocation();
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
keepCurrentLocationVisible = false;
zoomToCurrentLocation = false;
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, DEFAULT_ZOOM_LEVEL);
googleMap.moveCamera(cameraUpdate);
}
}
});
}
/**
* Gets the default LatLng.
*/
private LatLng getDefaultLatLng() {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(getActivity());
Track track = myTracksProviderUtils.getLastTrack();
if (track != null) {
Location location = myTracksProviderUtils.getLastValidTrackPoint(track.getId());
if (location != null) {
return new LatLng(location.getLatitude(), location.getLongitude());
}
}
return new LatLng(DEFAULT_LATITUDE, DEFAULT_LONGITUDE);
}
/**
* Returns true if the location is visible. Needs to run on the UI thread.
*
* @param location the location
*/
private boolean isLocationVisible(Location location) {
if (location == null || googleMap == null) {
return false;
}
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
return googleMap.getProjection().getVisibleRegion().latLngBounds.contains(latLng);
}
/**
* Sets the warning message.
*
* @param isGpsProviderEnabled true if gps provider is enabled
*/
private void setWarningMessage(boolean isGpsProviderEnabled) {
String message;
if (!isSelectedTrackRecording()) {
message = null;
} else {
if (isGpsProviderEnabled) {
boolean hasFix;
boolean hasGoodFix;
if (currentLocation == null) {
hasFix = false;
hasGoodFix = false;
} else {
hasFix = !LocationUtils.isLocationOld(currentLocation);
hasGoodFix = currentLocation.getAccuracy() <= minRequiredAccuracy;
}
if (!hasFix) {
message = getString(R.string.gps_wait_for_signal);
} else if (!hasGoodFix) {
message = getString(R.string.gps_wait_for_better_signal);
} else {
message = null;
}
} else {
String setting = getString(
GoogleLocationUtils.isAvailable(getActivity()) ? R.string.gps_google_location_settings
: R.string.gps_location_access);
message = getString(R.string.gps_disabled, setting);
}
}
if (message == null) {
messageTextView.setVisibility(View.GONE);
return;
}
messageTextView.setText(message);
messageTextView.setVisibility(View.VISIBLE);
if (isGpsProviderEnabled) {
messageTextView.setOnClickListener(null);
} else {
Toast.makeText(getActivity(), R.string.gps_not_found, Toast.LENGTH_LONG).show();
// Click to show the location source settings
messageTextView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = GoogleLocationUtils.isAvailable(getActivity()) ? new Intent(
GoogleLocationUtils.ACTION_GOOGLE_LOCATION_SETTINGS)
: new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
});
}
}
}