Files
OpenTracks/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java
T
Jimmy Shih 97f4103e67 merge heads
2012-04-09 14:07:04 -07:00

559 lines
16 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.GeoRect;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.mytracks.R;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.EnumSet;
/**
* The map view activity of the MyTracks application.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class MapActivity extends com.google.android.maps.MapActivity
implements View.OnTouchListener, View.OnClickListener,
TrackDataListener {
// Saved instance state keys:
// ---------------------------
private static final String KEY_CURRENT_LOCATION = "currentLocation";
private static final String KEY_KEEP_MY_LOCATION_VISIBLE = "keepMyLocationVisible";
private TrackDataHub dataHub;
/**
* True if the map should be scrolled so that the pointer is always in the
* visible area.
*/
private boolean keepMyLocationVisible;
/**
* True if we've already zoomed into the current location.
* Only relevant when {@link #keepMyLocationVisible} is true.
*/
private boolean myLocationWasZoomedIn;
/**
* The ID of a track on which we want to show a waypoint.
* The waypoint will be shown as soon as the track is loaded.
*/
private long showWaypointTrackId;
/**
* The ID of a waypoint which we want to show.
* The waypoint will be shown as soon as its track is loaded.
*/
private long showWaypointId;
/**
* The track that's currently selected.
* This differs from {@link TrackDataHub#getSelectedTrackId} in that this one is only set after
* actual track data has been received.
*/
private long selectedTrackId;
/**
* The current pointer location.
* This is kept to quickly center on it when the user requests.
*/
private Location currentLocation;
// UI elements:
// -------------
private RelativeLayout screen;
private MapView mapView;
private MapOverlay mapOverlay;
private LinearLayout messagePane;
private TextView messageText;
private LinearLayout busyPane;
/**
* 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
protected void onCreate(Bundle bundle) {
Log.d(TAG, "MapActivity.onCreate");
super.onCreate(bundle);
// The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// 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 MapOverlay(this);
mapView.getOverlays().add(mapOverlay);
mapView.setOnTouchListener(this);
mapView.setBuiltInZoomControls(true);
messagePane = (LinearLayout) findViewById(R.id.messagepane);
messageText = (TextView) findViewById(R.id.messagetext);
busyPane = (LinearLayout) findViewById(R.id.busypane);
}
@Override
protected void onRestoreInstanceState(Bundle bundle) {
Log.d(TAG, "MapActivity.onRestoreInstanceState");
if (bundle != null) {
super.onRestoreInstanceState(bundle);
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) {
showCurrentLocation();
}
} else {
currentLocation = null;
}
}
}
@Override
protected void onResume() {
Log.d(TAG, "MapActivity.onResume");
super.onResume();
dataHub = ((MyTracksApplication) getApplication()).getTrackDataHub();
dataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED,
ListenerDataType.POINT_UPDATES,
ListenerDataType.WAYPOINT_UPDATES,
ListenerDataType.LOCATION_UPDATES,
ListenerDataType.COMPASS_UPDATES));
}
@Override
protected void onSaveInstanceState(Bundle outState) {
Log.d(TAG, "MapActivity.onSaveInstanceState");
outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible);
if (currentLocation != null) {
outState.putParcelable(KEY_CURRENT_LOCATION, currentLocation);
}
super.onSaveInstanceState(outState);
}
@Override
protected void onPause() {
Log.d(TAG, "MapActivity.onPause");
dataHub.unregisterTrackDataListener(this);
dataHub = null;
super.onPause();
}
/**
* 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 = LocationUtils.getGeoPoint(location);
return r.contains(geoPoint);
}
/**
* Centers (and keeps centered) the map on the current location.
*/
public void showMyLocation() {
dataHub.forceUpdateLocation();
keepMyLocationVisible = true;
myLocationWasZoomedIn = false;
if (currentLocation != null) {
showCurrentLocation();
}
}
/**
* 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 (mapOverlay == null || mapView == null) {
return;
}
mapOverlay.setMyLocation(currentLocation);
mapView.postInvalidate();
if (currentLocation != null && keepMyLocationVisible && !locationIsVisible(currentLocation)) {
GeoPoint geoPoint = LocationUtils.getGeoPoint(currentLocation);
MapController controller = mapView.getController();
controller.animateTo(geoPoint);
if (!myLocationWasZoomedIn && mapView.getZoomLevel() < 18) {
// Only zoom in the first time we show the location.
myLocationWasZoomedIn = true;
controller.setZoom(18);
}
}
}
@Override
public void onTrackUpdated(Track track) {
// We don't care.
}
/**
* Zooms and pans the map so that the given track is visible.
*
* @param track the track
*/
private void zoomMapToBoundaries(Track track) {
if (mapView == null) {
return;
}
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 (LocationUtils.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) {
MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(this);
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();
}
}
/**
* Zooms and pans the map so that the given waypoint is visible, when the given track is loaded.
* If the track is already loaded, it does that immediately.
*
* @param trackId the ID of the track on which to show the waypoint
* @param waypointId the ID of the waypoint to show
*/
public void showWaypoint(long trackId, long waypointId) {
synchronized (this) {
if (trackId == selectedTrackId) {
showWaypoint(waypointId);
return;
}
showWaypointTrackId = trackId;
showWaypointId = waypointId;
}
}
/**
* Does the proper zooming/panning for a just-loaded track.
* This may be either zooming to a waypoint that has been previously selected, or
* zooming to the whole track.
*
* @param track the loaded track
*/
private void zoomLoadedTrack(Track track) {
synchronized (this) {
if (track.getId() == showWaypointTrackId) {
// There's a waypoint to show in this track.
showWaypoint(showWaypointId);
showWaypointId = 0L;
showWaypointTrackId = 0L;
} else {
// Zoom out to show the whole track.
zoomMapToBoundaries(track);
}
}
}
@Override
public void onSelectedTrackChanged(final Track track, final boolean isRecording) {
runOnUiThread(new Runnable() {
@Override
public void run() {
boolean trackSelected = track != null;
mapOverlay.setTrackDrawingEnabled(trackSelected);
if (trackSelected) {
busyPane.setVisibility(View.VISIBLE);
synchronized (this) {
// Need to get the track ID only at this point, to prevent a race condition
// among showWaypoint, zoomLoadedTrack and dataHub.loadTrack.
selectedTrackId = track.getId();
zoomLoadedTrack(track);
}
mapOverlay.setShowEndMarker(!isRecording);
busyPane.setVisibility(View.GONE);
}
mapView.invalidate();
}
});
}
/**
* Returns whether the map is currently in satellite view mode.
*/
public boolean isSatelliteView() {
return mapView.isSatellite();
}
/**
* Changes whether the map should be in satellite view mode.
*/
public void setSatelliteView(boolean sat) {
mapView.setSatellite(sat);
}
@Override
public void onClick(View v) {
if (v == messagePane) {
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}
}
/**
* 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 onProviderStateChange(ProviderState state) {
final int messageId;
final boolean isGpsDisabled;
switch (state) {
case DISABLED:
messageId = R.string.gps_need_to_enable;
isGpsDisabled = true;
break;
case NO_FIX:
case BAD_FIX:
messageId = R.string.gps_wait_for_fix;
isGpsDisabled = false;
break;
case GOOD_FIX:
// Nothing to show.
messageId = -1;
isGpsDisabled = false;
break;
default:
throw new IllegalArgumentException("Unexpected state: " + state);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
if (messageId != -1) {
messageText.setText(messageId);
messagePane.setVisibility(View.VISIBLE);
if (isGpsDisabled) {
// Give a warning about this state.
Toast.makeText(MapActivity.this,
R.string.gps_not_found,
Toast.LENGTH_LONG).show();
// Make clicking take the user to the location settings.
messagePane.setOnClickListener(MapActivity.this);
} else {
messagePane.setOnClickListener(null);
}
} else {
messagePane.setVisibility(View.GONE);
}
screen.requestLayout();
}
});
}
@Override
public void onCurrentLocationChanged(Location location) {
currentLocation = location;
showCurrentLocation();
}
@Override
public void onCurrentHeadingChanged(double heading) {
synchronized (this) {
if (mapOverlay.setHeading((float) heading)) {
mapView.postInvalidate();
}
}
}
@Override
public void clearWaypoints() {
mapOverlay.clearWaypoints();
}
@Override
public void onNewWaypoint(Waypoint waypoint) {
if (LocationUtils.isValidLocation(waypoint.getLocation())) {
// TODO: Optimize locking inside addWaypoint
mapOverlay.addWaypoint(waypoint);
}
}
@Override
public void onNewWaypointsDone() {
mapView.postInvalidate();
}
@Override
public void clearTrackPoints() {
mapOverlay.clearPoints();
}
@Override
public void onNewTrackPoint(Location loc) {
mapOverlay.addLocation(loc);
}
@Override
public void onSegmentSplit() {
mapOverlay.addSegmentSplit();
}
@Override
public void onSampledOutTrackPoint(Location loc) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
mapView.postInvalidate();
}
@Override
public boolean onUnitsChanged(boolean metric) {
// We don't care.
return false;
}
@Override
public boolean onReportSpeedChanged(boolean reportSpeed) {
// We don't care.
return false;
}
}