Files
OpenTracks/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java
T
2011-02-22 22:10:03 -08:00

1095 lines
35 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.GeoRect;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.database.Cursor;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.provider.Settings;
import android.util.Log;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.Window;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnCreateContextMenuListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
/**
* The map view activity of the MyTracks application.
*
* @author Leif Hendrik Wilden
*/
public class MyTracksMap extends MapActivity
implements View.OnTouchListener, View.OnClickListener,
SharedPreferences.OnSharedPreferenceChangeListener {
private static final int TRACKPOINT_BUFFER_SIZE = 1024;
// Saved instance state keys:
// ---------------------------
public static final String KEY_CURRENT_LOCATION = "currentLocation";
public static final String KEY_KEEP_MY_LOCATION_VISIBLE =
"keepMyLocationVisible";
public static final String KEY_HAVE_GOOD_FIX = "haveGoodFix";
/**
* The ID of the currently selected track (or -1 if nothing selected).
*/
private long selectedTrackId = -1;
/**
* The id of the currently recording track.
*/
private long recordingTrackId = -1;
/**
* True if the map should be scrolled so that the pointer is always in the
* visible area.
*/
private boolean keepMyLocationVisible;
/**
* Id of the first location that was seen when reading tracks from the
* provider.
*/
private long firstSeenLocationId = -1;
/**
* Id of the last location that was seen when reading tracks from the
* provider. This is used to determine which locations are new compared to the
* last time the mapOverlay was updated.
*/
private long lastSeenLocationId = -1;
/**
* Magnetic variation.
*/
private double variation;
/**
* From the shared preferences.
*/
private int minRequiredAccuracy =
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY;
/**
* True, if the application thinks it has a good fix, i.e. accuracy is better
* than the required accuracy.
*/
private boolean haveGoodFix;
/**
* The current pointer location.
*/
private Location currentLocation;
/**
* A thread with a looper. Post to updateTrackHandler to execute
* {@link Runnable}s on this thread.
*/
private HandlerThread updateTrackThread;
/**
* Handler for updateTrackThread.
*/
private Handler updateTrackHandler;
private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences;
/**
* A runnable that updates the track from the provider (looking for points
* added after "lastSeenLocationId").
*/
private final Runnable updateTrackRunnable = new Runnable() {
@Override
public void run() {
if (!isATrackSelected()) {
return;
}
readAllNewTrackPoints();
}
};
/**
* A runnable that restores all track points from the provider.
*/
private Runnable restoreTrackRunnable = new Runnable() {
@Override
public void run() {
if (!isATrackSelected()) {
return;
}
mapOverlay.clearPoints();
firstSeenLocationId = -1;
lastSeenLocationId = -1;
readAllNewTrackPoints();
}
};
/**
* A runnable that restores all waypoints from the provider.
*/
private final Runnable restoreWaypointsRunnable = new Runnable() {
@Override
public void run() {
if (!isATrackSelected()) {
return;
}
Cursor cursor = null;
mapOverlay.clearWaypoints();
try {
// We will silently drop extra waypoints to make the app responsive.
// TODO: Try to only load the waypoints in the view port.
cursor = providerUtils.getWaypointsCursor(
selectedTrackId, 0,
MyTracksConstants.MAX_DISPLAYED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
do {
Waypoint waypoint = providerUtils.createWaypoint(cursor);
if (MyTracksUtils.isValidLocation(waypoint.getLocation())) {
mapOverlay.addWaypoint(waypoint);
}
} while (cursor.moveToNext());
}
} catch (RuntimeException e) {
Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
mapView.postInvalidate();
}
};
/**
* A runnable intended to be posted to the {@code #updateTrackThread} after
* the selected track changes. It will post to the UI thread to update
* the screen elements and move the map to show the selected track.
*/
private final Runnable setSelectedTrackRunnable = new Runnable() {
@Override
public void run() {
uiHandler.post(new Runnable() {
public void run() {
showTrack(selectedTrackId);
mapOverlay.setTrackDrawingEnabled(isATrackSelected());
mapOverlay.setShowEndMarker(!isRecordingSelected());
mapView.invalidate();
busyPane.setVisibility(View.GONE);
updateOptionsButton();
}
});
}
};
// UI elements:
// -------------
private RelativeLayout screen;
private MapView mapView;
private MyTracksOverlay mapOverlay;
private LinearLayout messagePane;
private TextView messageText;
private LinearLayout busyPane;
private ImageButton optionsBtn;
private MenuItem myLocation;
private MenuItem toggleLayers;
private SensorManager sensorManager;
private LocationManager locationManager;
private ContentObserver observer;
private ContentObserver waypointObserver;
/** Handler for callbacks to the UI thread */
private final Handler uiHandler = new Handler();
/**
* We are not displaying driving directions. Just an arbitrary track that is
* not associated to any licensed mapping data. Therefore it should be okay to
* return false here and still comply with the terms of service.
*/
@Override
protected boolean isRouteDisplayed() {
return false;
}
/**
* We are displaying a location. This needs to return true in order to comply
* with the terms of service.
*/
@Override
protected boolean isLocationDisplayed() {
return true;
}
// Application life cycle:
// ------------------------
@Override
public void onCreate(Bundle bundle) {
Log.d(MyTracksConstants.TAG, "MyTracksMap.onCreate");
super.onCreate(bundle);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
providerUtils = MyTracksProviderUtils.Factory.get(this);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Inflate the layout:
setContentView(R.layout.mytracks_layout);
// Remove the window's background because the MapView will obscure it
getWindow().setBackgroundDrawable(null);
// Set up a map overlay:
screen = (RelativeLayout) findViewById(R.id.screen);
mapView = (MapView) findViewById(R.id.map);
mapView.requestFocus();
mapOverlay = new MyTracksOverlay(this);
mapView.getOverlays().add(mapOverlay);
mapView.setOnTouchListener(this);
messagePane = (LinearLayout) findViewById(R.id.messagepane);
messageText = (TextView) findViewById(R.id.messagetext);
busyPane = (LinearLayout) findViewById(R.id.busypane);
optionsBtn = (ImageButton) findViewById(R.id.showOptions);
optionsBtn.setOnCreateContextMenuListener(contextMenuListener);
optionsBtn.setOnClickListener(this);
setupZoomControls();
// Get the sensor and location managers:
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
locationManager =
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
updateTrackThread = new HandlerThread("updateTrackThread");
updateTrackThread.start();
updateTrackHandler = new Handler(updateTrackThread.getLooper());
// Register observer for the track point provider:
Handler contentHandler = new Handler();
observer = new ContentObserver(contentHandler) {
@Override
public void onChange(boolean selfChange) {
Log.d(MyTracksConstants.TAG, "MyTracksMap: ContentObserver.onChange");
if (!isRecordingSelected()) {
// No track, or one other than the recording track is selected,
// don't bother.
return;
}
// Update can potentially be lengthy, put it in its own thread:
updateTrackHandler.post(updateTrackRunnable);
super.onChange(selfChange);
}
};
waypointObserver = new ContentObserver(contentHandler) {
@Override
public void onChange(boolean selfChange) {
Log.d(MyTracksConstants.TAG,
"MyTracksMap: ContentObserver.onChange waypoints");
if (!isATrackSelected()) {
return;
}
updateTrackHandler.post(restoreWaypointsRunnable);
super.onChange(selfChange);
}
};
// Read shared preferences and register change listener.
sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (sharedPreferences != null) {
reloadSharedPreferences(sharedPreferences, null);
updateOptionsButton();
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
}
}
@Override
protected void onDestroy() {
Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy");
if (updateTrackThread != null) {
ApiFeatures.getInstance().getApiPlatformAdapter().stopHandlerThread(
updateTrackThread);
}
if (sharedPreferences != null) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
}
super.onDestroy();
}
/**
* Returns whether there's a track currently selected for display.
*/
private boolean isATrackSelected() {
return selectedTrackId >= 0;
}
/**
* Returns whether we're currently recording the same track that's selected
* for display.
*/
private boolean isRecordingSelected() {
return isATrackSelected() && selectedTrackId == recordingTrackId;
}
protected void setupZoomControls() {
mapView.setBuiltInZoomControls(true);
}
@Override
protected void onStart() {
// Called after onCreate or onStop.
// Will be followed by onRestart.
Log.d(MyTracksConstants.TAG, "MyTracksMap.onStart");
super.onStart();
}
@Override
protected void onStop() {
// Called when activity is no longer visible to user.
// Next either onStart, onDestroy or nothing will be called.
// This method may never be called in low memory situations.
Log.d(MyTracksConstants.TAG, "MyTracksMap.onStop");
super.onStop();
}
@Override
protected void onRestart() {
// Called when the current activity is being re-displayed.
// Will be followed by onResume.
Log.d(MyTracksConstants.TAG, "MyTracksMap.onRestart");
super.onRestart();
}
@Override
protected void onPause() {
// Called when activity is going into the background, but has not (yet) been
// killed. Shouldn't block longer than approx. 2 seconds.
Log.d(MyTracksConstants.TAG, "MyTracksMap.onPause");
unregisterLocationAndSensorListeners();
unregisterContentObservers();
super.onPause();
}
@Override
protected void onResume() {
// Called when the current activity is being displayed or re-displayed
// to the user.
Log.d(MyTracksConstants.TAG, "MyTracksMap.onResume");
super.onResume();
// Reload all preferences as they might have changed since last run.
reloadSharedPreferences(sharedPreferences, null);
// Make sure any updates that might have happened are propagated to the
// Map overlay:
observer.onChange(false);
waypointObserver.onChange(false);
registerContentObservers();
registerLocationAndSensorListeners();
if (locationManager.isProviderEnabled(MyTracksConstants.GPS_PROVIDER)) {
messageText.setText(R.string.wait_for_fix);
messagePane.setOnClickListener(null);
} else {
messageText.setText(R.string.status_enable_gps);
messagePane.setVisibility(View.VISIBLE);
messagePane.setOnClickListener(this);
screen.requestLayout();
}
// While this activity was paused the user may have deleted the selected
// track. In that case the map overlay needs to be cleared:
if (isATrackSelected() && !providerUtils.trackExists(selectedTrackId)) {
// The recording track must have been deleted meanwhile.
mapOverlay.setTrackDrawingEnabled(false);
mapView.invalidate();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
Log.d(MyTracksConstants.TAG, "MyTracksMap.onSaveInstanceState");
outState.putBoolean(KEY_HAVE_GOOD_FIX, haveGoodFix);
outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible);
if (currentLocation != null) {
outState.putParcelable(KEY_CURRENT_LOCATION, currentLocation);
}
super.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(Bundle bundle) {
Log.d(MyTracksConstants.TAG, "MyTracksMap.onRestoreInstanceState");
if (bundle != null) {
super.onRestoreInstanceState(bundle);
haveGoodFix = bundle.getBoolean(KEY_HAVE_GOOD_FIX, false);
keepMyLocationVisible =
bundle.getBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, false);
if (bundle.containsKey(KEY_CURRENT_LOCATION)) {
currentLocation = (Location) bundle.getParcelable(KEY_CURRENT_LOCATION);
if (currentLocation != null) {
setVariation(currentLocation);
showCurrentLocation();
}
} else {
currentLocation = null;
}
}
}
// Utility functions:
// -------------------
/**
* Toggles between satellite and map view.
*/
public void toggleLayer() {
mapView.setSatellite(!mapView.isSatellite());
}
/**
* Registers to receive location updates from the GPS location provider and
* sensor updated from the compass.
*/
void registerLocationAndSensorListeners() {
if (locationManager != null) {
LocationProvider gpsProvider =
locationManager.getProvider(MyTracksConstants.GPS_PROVIDER);
if (gpsProvider == null) {
alert(getString(R.string.error_no_gps_location_provider));
return;
} else {
Log.d(MyTracksConstants.TAG, "MyTracksMap: Using location provider "
+ gpsProvider.getName());
}
locationManager.requestLocationUpdates(gpsProvider.getName(),
0 /*minTime*/, 0 /*minDist*/, locationListener);
try {
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, locationListener);
} catch (RuntimeException e) {
// If anything at all goes wrong with getting a cell location do not
// abort. Cell location is not essential to this app.
Log.w(MyTracksConstants.TAG,
"Could not register network location listener.");
}
}
if (sensorManager == null) {
return;
}
Sensor compass = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
if (compass == null) {
return;
}
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Now registering sensor listeners.");
sensorManager.registerListener(
sensorListener, compass, SensorManager.SENSOR_DELAY_UI);
}
/**
* Unregisters all location and sensor listeners
*/
void unregisterLocationAndSensorListeners() {
if (locationManager != null) {
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Now unregistering location listeners.");
locationManager.removeUpdates(locationListener);
}
if (sensorManager != null) {
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Now unregistering sensor listeners.");
sensorManager.unregisterListener(sensorListener);
}
}
/**
* Registers the content observer for the map overlay.
*/
private void registerContentObservers() {
getContentResolver().registerContentObserver(
TrackPointsColumns.CONTENT_URI, false /* notifyForDescendents */,
observer);
getContentResolver().registerContentObserver(
WaypointsColumns.CONTENT_URI, false /* notifyForDescendents */,
waypointObserver);
}
/**
* Unregisters the content observer for the map overlay.
*/
private void unregisterContentObservers() {
getContentResolver().unregisterContentObserver(observer);
getContentResolver().unregisterContentObserver(waypointObserver);
}
/**
* Shows the options button if a track is selected, or hide it if not.
*/
private void updateOptionsButton() {
optionsBtn.setVisibility(
isATrackSelected() ? View.VISIBLE : View.INVISIBLE);
}
/**
* Tests if a location is visible.
*
* @param location a given location
* @return true if the given location is within the visible map area
*/
private boolean locationIsVisible(Location location) {
if (location == null || mapView == null) {
return false;
}
GeoPoint center = mapView.getMapCenter();
int latSpan = mapView.getLatitudeSpan();
int lonSpan = mapView.getLongitudeSpan();
// Bottom of map view is obscured by zoom controls/buttons.
// Subtract a margin from the visible area:
GeoPoint marginBottom = mapView.getProjection().fromPixels(
0, mapView.getHeight());
GeoPoint marginTop = mapView.getProjection().fromPixels(0,
mapView.getHeight()
- mapView.getZoomButtonsController().getZoomControls().getHeight());
int margin =
Math.abs(marginTop.getLatitudeE6() - marginBottom.getLatitudeE6());
GeoRect r = new GeoRect(center, latSpan, lonSpan);
r.top += margin;
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(location);
return r.contains(geoPoint);
}
/**
* Moves the location pointer to the current location and center the map if
* the current location is outside the visible area.
*/
private void showCurrentLocation() {
if (currentLocation == null || mapOverlay == null || mapView == null) {
return;
}
mapOverlay.setMyLocation(currentLocation);
mapView.invalidate();
if (keepMyLocationVisible && !locationIsVisible(currentLocation)) {
MapController controller = mapView.getController();
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(currentLocation);
controller.animateTo(geoPoint);
}
}
/**
* Zooms and pans the map so that the given track is visible.
*
* @param trackId a given track ID
*/
public void showTrack(long trackId) {
if (mapView == null) {
return;
}
Track track = providerUtils.getTrack(trackId);
if (track == null || track.getNumberOfPoints() < 2) {
return;
}
TripStatistics stats = track.getStatistics();
int bottom = stats.getBottom();
int left = stats.getLeft();
int latSpanE6 = stats.getTop() - bottom;
int lonSpanE6 = stats.getRight() - left;
if (latSpanE6 > 0
&& latSpanE6 < 180E6
&& lonSpanE6 > 0
&& lonSpanE6 < 360E6) {
keepMyLocationVisible = false;
GeoPoint center = new GeoPoint(
bottom + latSpanE6 / 2,
left + lonSpanE6 / 2);
if (MyTracksUtils.isValidGeoPoint(center)) {
mapView.getController().setCenter(center);
mapView.getController().zoomToSpan(latSpanE6, lonSpanE6);
}
}
}
/**
* Zooms and pans the map so that the given waypoint is visible.
*/
public void showWaypoint(long waypointId) {
Waypoint wpt = providerUtils.getWaypoint(waypointId);
if (wpt != null && wpt.getLocation() != null) {
keepMyLocationVisible = false;
GeoPoint center = new GeoPoint(
(int) (wpt.getLocation().getLatitude() * 1E6),
(int) (wpt.getLocation().getLongitude() * 1E6));
mapView.getController().setCenter(center);
mapView.getController().setZoom(20);
mapView.invalidate();
}
}
/**
* Sets the selected track and zoom and pan the map so that it is visible.
*
* @param trackId a given track id
*/
public void setSelectedTrack(final long trackId) {
Log.d(MyTracksConstants.TAG, "MyTracksMap.setSelectedTrack: "
+ "selectedTrackId = " + selectedTrackId + ", trackId = " + trackId);
if (selectedTrackId == trackId) {
// Selected track did not change, nothing to do.
mapOverlay.setTrackDrawingEnabled(isATrackSelected());
updateOptionsButton();
mapView.invalidate();
return;
}
if (trackId < 0) {
// Remove selection.
selectedTrackId = -1;
mapOverlay.setTrackDrawingEnabled(false);
mapOverlay.clearWaypoints();
updateOptionsButton();
mapView.invalidate();
return;
}
busyPane.setVisibility(View.VISIBLE);
selectedTrackId = trackId;
loadSelectedTrack();
}
private void loadSelectedTrack() {
updateTrackHandler.post(restoreTrackRunnable);
updateTrackHandler.post(restoreWaypointsRunnable);
updateTrackHandler.post(setSelectedTrackRunnable);
}
/**
* Displays an alert for a few seconds.
*
* @param txt The text to be displayed
*/
public void alert(String txt) {
Toast.makeText(this, txt, Toast.LENGTH_LONG).show();
}
public void launchMyLocationSettings() {
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}
public void setVariation(Location location) {
long timestamp = location.getTime();
if (timestamp == 0) {
// Hack for Samsung phones which don't populate the time field
timestamp = System.currentTimeMillis();
}
GeomagneticField field = new GeomagneticField(
(float) location.getLatitude(),
(float) location.getLongitude(),
(float) location.getAltitude(),
timestamp);
variation = field.getDeclination();
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Variation reset to " + variation + " degrees.");
}
public MyTracksOverlay getMapOverlay() {
return mapOverlay;
}
public MapView getMapView() {
return mapView;
}
// Event listeners:
// -----------------
private final OnCreateContextMenuListener contextMenuListener =
new OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
menu.setHeaderTitle(R.string.tracklist_this_track);
menu.add(0, MyTracksConstants.MENU_EDIT, 0,
R.string.tracklist_edit_track);
if (!isRecordingSelected()) {
menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0,
R.string.tracklist_send_to_google);
SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0,
R.string.tracklist_share_track);
share.add(0, MyTracksConstants.MENU_SHARE_LINK, 0,
R.string.tracklist_share_linkfusion);
share.add(0, MyTracksConstants.MENU_SHARE_GPX_FILE, 0,
R.string.tracklist_share_gpx_file);
share.add(0, MyTracksConstants.MENU_SHARE_KML_FILE, 0,
R.string.tracklist_share_kml_file);
share.add(0, MyTracksConstants.MENU_SHARE_CSV_FILE, 0,
R.string.tracklist_share_csv_file);
share.add(0, MyTracksConstants.MENU_SHARE_TCX_FILE, 0,
R.string.tracklist_share_tcx_file);
SubMenu save = menu.addSubMenu(0,
MyTracksConstants.MENU_WRITE_TO_SD_CARD, 0,
R.string.tracklist_write_to_sd);
save.add(0, MyTracksConstants.MENU_SAVE_GPX_FILE, 0,
R.string.tracklist_save_as_gpx);
save.add(0, MyTracksConstants.MENU_SAVE_KML_FILE, 0,
R.string.tracklist_save_as_kml);
save.add(0, MyTracksConstants.MENU_SAVE_CSV_FILE, 0,
R.string.tracklist_save_as_csv);
save.add(0, MyTracksConstants.MENU_SAVE_TCX_FILE, 0,
R.string.tracklist_save_as_tcx);
menu.add(0, MyTracksConstants.MENU_CLEAR_MAP, 0,
R.string.tracklist_clear_map);
menu.add(0, MyTracksConstants.MENU_DELETE, 0,
R.string.tracklist_delete_track);
}
}
};
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
if (!super.onMenuItemSelected(featureId, item)) {
if (isATrackSelected()) {
MyTracks.getInstance().onActivityResult(
MyTracksConstants.getActionFromMenuId(item.getItemId()), RESULT_OK,
new Intent());
return true;
}
}
return false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
myLocation = menu.add(0, MyTracksConstants.MENU_MY_LOCATION, 0,
R.string.mylocation);
myLocation.setIcon(android.R.drawable.ic_menu_mylocation);
toggleLayers = menu.add(0, MyTracksConstants.MENU_TOGGLE_LAYERS, 0,
R.string.switch_to_sat);
toggleLayers.setIcon(android.R.drawable.ic_menu_mapmode);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
toggleLayers.setTitle(mapView.isSatellite() ?
R.string.switch_to_map : R.string.switch_to_sat);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MyTracksConstants.MENU_MY_LOCATION: {
Location loc = MyTracks.getInstance().getCurrentLocation();
if (loc != null) {
currentLocation = loc;
setVariation(currentLocation);
mapOverlay.setMyLocation(loc);
mapView.invalidate();
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(loc);
MapController controller = mapView.getController();
controller.animateTo(geoPoint);
if (mapView.getZoomLevel() < 18) {
controller.setZoom(18);
}
keepMyLocationVisible = true;
}
return true;
}
case MyTracksConstants.MENU_TOGGLE_LAYERS: {
toggleLayer();
return true;
}
}
return super.onOptionsItemSelected(item);
}
@Override
public void onClick(View v) {
if (v == messagePane) {
launchMyLocationSettings();
} else if (v == optionsBtn) {
optionsBtn.performLongClick();
}
}
/**
* We want the pointer to become visible again in case of the next location
* update:
*/
@Override
public boolean onTouch(View view, MotionEvent event) {
if (keepMyLocationVisible && event.getAction() == MotionEvent.ACTION_MOVE) {
if (!locationIsVisible(currentLocation)) {
keepMyLocationVisible = false;
}
}
return false;
}
@Override
public void onSharedPreferenceChanged(
final SharedPreferences sharedPreferences, final String key) {
Log.d(MyTracksConstants.TAG,
"MyTracksMap.onSharedPreferenceChanged: " + key);
if (key != null) {
uiHandler.post(new Runnable() {
@Override
public void run() {
reloadSharedPreferences(sharedPreferences, key);
}
});
}
}
private final LocationListener locationListener = new LocationListener() {
@Override
public void onProviderEnabled(String provider) {
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
messageText.setText(R.string.wait_for_fix);
}
}
@Override
public void onProviderDisabled(String provider) {
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
messageText.setText(R.string.status_enable_gps);
messagePane.setVisibility(View.VISIBLE);
messagePane.setOnClickListener(MyTracksMap.this);
screen.requestLayout();
}
}
@Override
public void onLocationChanged(Location location) {
if (location.getProvider().equals(MyTracksConstants.GPS_PROVIDER)) {
// Recalculate the variation if there was a jump in location > 1km:
if (currentLocation == null ||
location.distanceTo(currentLocation) > 1000) {
setVariation(location);
}
currentLocation = location;
boolean haveGoodFixNow =
currentLocation.getAccuracy() < minRequiredAccuracy;
if (haveGoodFixNow != haveGoodFix) {
haveGoodFix = haveGoodFixNow;
messagePane.setVisibility(haveGoodFix ? View.GONE : View.VISIBLE);
screen.requestLayout();
}
showCurrentLocation();
} else {
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Network location update received.");
}
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
switch (status) {
case LocationProvider.OUT_OF_SERVICE:
case LocationProvider.TEMPORARILY_UNAVAILABLE:
haveGoodFix = false;
messagePane.setVisibility(View.VISIBLE);
screen.requestLayout();
break;
}
}
}
};
private final SensorEventListener sensorListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent se) {
synchronized (this) {
float magneticHeading = se.values[0];
double heading = magneticHeading + variation;
if (mapOverlay.setHeading((float) heading)) {
mapView.invalidate();
}
}
}
@Override
public void onAccuracyChanged(Sensor s, int accuracy) {
// do nothing
}
};
private void reloadSharedPreferences(SharedPreferences sharedPreferences,
String key) {
if (key == null ||
key.equals(getString(R.string.min_required_accuracy_key))) {
minRequiredAccuracy = sharedPreferences.getInt(
getString(R.string.min_required_accuracy_key),
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
}
if (key == null || key.equals(getString(R.string.recording_track_key))) {
recordingTrackId = sharedPreferences.getLong(
getString(R.string.recording_track_key), -1);
}
if (key == null || key.equals(getString(R.string.selected_track_key))) {
setSelectedTrack(sharedPreferences.getLong(
getString(R.string.selected_track_key), -1));
}
// Show end marker if the track has been selected and is not recording.
// Note: This check must be *after* a call to setSelectedTrack(...) above.
if (isATrackSelected()) {
mapOverlay.setShowEndMarker(!isRecordingSelected());
mapView.postInvalidate();
}
}
private void readAllNewTrackPoints() {
int numPoints = mapOverlay.getNumLocations();
if (numPoints >= MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS) {
// We're about to exceed the maximum allowed number of points, so reload
// the whole track with fewer points (the sampling frequency will be
// lower).
loadSelectedTrack();
return;
}
// Keep a copy of selectedTrackId, because it can change asynchronously.
long currentSelectedTrackId = selectedTrackId;
long lastStoredLocationId =
providerUtils.getLastLocationId(currentSelectedTrackId);
int samplingFrequency = -1;
Location location = new Location("");
while (currentSelectedTrackId == selectedTrackId) {
Cursor cursor = null;
try {
cursor = providerUtils.getLocationsCursor(currentSelectedTrackId,
lastSeenLocationId + 1, TRACKPOINT_BUFFER_SIZE, false);
if (cursor == null || !cursor.moveToFirst()) {
// No (more) data
break;
}
final int idColumnIdx = cursor.getColumnIndexOrThrow(
TrackPointsColumns._ID);
do {
long locationId = cursor.getLong(idColumnIdx);
lastSeenLocationId = locationId;
if (firstSeenLocationId == -1) {
// This was our first point, keep its ID
firstSeenLocationId = locationId;
}
if (samplingFrequency == -1) {
// Now we already have at least one point, calculate the sampling
// frequency
long numTotalPoints = lastStoredLocationId - firstSeenLocationId;
samplingFrequency = (int) (1 + numTotalPoints
/ MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS);
// TODO: This shouldn't happen after adding currentSelectedTrackId,
// but just to be safe until we have 100% confidence.
if (samplingFrequency <= 0) {
Log.w(MyTracksConstants.TAG,
"readAllNewTrackPoints: samplingFreq <= 0, numTotalPoints = "
+ numTotalPoints + ", trackId = " + currentSelectedTrackId);
samplingFrequency = 1;
}
}
providerUtils.fillLocation(cursor, location);
// Include a point if it fits one of the following criteria:
// - Has the mod for the sampling frequency (includes first point).
// - Is the last point and we are not recording this track.
// - The point is a segment split
if (numPoints % samplingFrequency == 0 ||
(!isRecordingSelected() && locationId == lastStoredLocationId) ||
!MyTracksUtils.isValidLocation(location)) {
// No need to allocate a new location (we can safely reuse the existing).
mapOverlay.addLocation(location);
}
numPoints++;
} while (cursor.moveToNext() &&
currentSelectedTrackId == selectedTrackId);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
mapView.postInvalidate();
}
}