Update to map view v2.

This commit is contained in:
Jimmy Shih
2012-10-20 16:18:50 -07:00
parent 2a676dbc9a
commit 9eca3df172
70 changed files with 1291 additions and 2401 deletions
@@ -556,7 +556,7 @@ public class ChartView extends View {
float x = getX(getWaypointXValue(waypoint));
canvas.drawLine(
x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
canvas.translate(x - (float) (markerWidth * Constants.WAYPOINT_X_OFFSET_PERCENTAGE),
canvas.translate(x - (float) (markerWidth * MapOverlay.WAYPOINT_X_ANCHOR),
topBorder + spacer);
if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
statisticsMarker.draw(canvas);
@@ -126,10 +126,6 @@ public abstract class Constants {
public static final String SETTINGS_NAME = "SettingsActivity";
public static final double WAYPOINT_X_OFFSET_PERCENTAGE = 13 / 48.0;
public static final double MARKER_Y_OFFSET_PERCENTAGE = 91 / 96.0;
/**
* This is an abstract utility class.
*/
@@ -19,30 +19,22 @@ package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.maps.TrackPathPainter;
import com.google.android.apps.mytracks.maps.TrackPathPainterFactory;
import com.google.android.apps.mytracks.maps.TrackPathUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.maps.TrackPath;
import com.google.android.apps.mytracks.maps.TrackPathFactory;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.BitmapDescriptors;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.Model;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.util.Log;
@@ -56,41 +48,33 @@ import java.util.concurrent.BlockingQueue;
*
* @author Leif Hendrik Wilden
*/
public class MapOverlay extends Overlay {
public class MapOverlay {
public static final float WAYPOINT_X_ANCHOR = 13f / 48f;
private static final float WAYPOINT_Y_ANCHOR = 43f / 48f;
private static final float MARKER_X_ANCHOR = 50f / 96f;
private static final float MARKER_Y_ANCHOR = 90f / 96f;
private static final int INITIAL_LOCATIONS_SIZE = 1024;
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.getKey(context, R.string.track_color_mode_key).equals(key)) {
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
if (key == null
|| PreferencesUtils.getKey(context, R.string.track_color_mode_key).equals(key)) {
trackPath = TrackPathFactory.getTrackPath(context);
}
}
};
private final Context context;
private final List<CachedLocation> locations;
private final BlockingQueue<CachedLocation> pendingLocations;
private final List<Waypoint> waypoints;
private final List<CachedLocation> points;
private final BlockingQueue<CachedLocation> pendingPoints;
private final Drawable[] arrows;
private final int arrowWidth;
private final int arrowHeight;
private final Drawable statsMarker;
private final Drawable waypointMarker;
private final Drawable startMarker;
private final Drawable endMarker;
private final int markerWidth;
private final int markerHeight;
private final Paint errorCirclePaint;
private TrackPathPainter trackPathPainter;
private boolean trackDrawingEnabled;
private boolean showEndMarker = true;
private int headingIndex = 0;
private Location myLocation;
private GeoPoint lastReferencePoint;
private Rect lastViewRect;
private TrackPath trackPath;
/**
* A pre-processed {@link Location} to speed up drawing.
@@ -100,7 +84,7 @@ public class MapOverlay extends Overlay {
public static class CachedLocation {
private final boolean valid;
private final GeoPoint geoPoint;
private final LatLng latLng;
private final int speed;
/**
@@ -108,7 +92,7 @@ public class MapOverlay extends Overlay {
*/
public CachedLocation() {
this.valid = false;
this.geoPoint = null;
this.latLng = null;
this.speed = -1;
}
@@ -117,7 +101,7 @@ public class MapOverlay extends Overlay {
*/
public CachedLocation(Location location) {
this.valid = LocationUtils.isValidLocation(location);
this.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null;
this.latLng = valid ? new LatLng(location.getLatitude(), location.getLongitude()) : null;
this.speed = (int) Math.floor(location.getSpeed() * UnitConversions.MS_TO_KMH);
}
@@ -128,94 +112,52 @@ public class MapOverlay extends Overlay {
return valid;
}
/**
* Gets the {@link GeoPoint}.
*/
public GeoPoint getGeoPoint() {
return geoPoint;
}
/**
* Gets the speed in kilometers per hour.
*/
public int getSpeed() {
return speed;
}
/**
* Gets the LatLng.
*/
public LatLng getLatLng() {
return latLng;
}
};
public MapOverlay(Context context) {
this.context = context;
this.waypoints = new ArrayList<Waypoint>();
this.points = new ArrayList<CachedLocation>(1024);
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
this.locations = new ArrayList<CachedLocation>(INITIAL_LOCATIONS_SIZE);
this.pendingLocations = new ArrayBlockingQueue<CachedLocation>(
Constants.MAX_DISPLAYED_TRACK_POINTS, true);
/*
* TODO: Use animation rather than individual resources for each arrow
* direction.
*/
final Resources resources = context.getResources();
arrows = new Drawable[] { resources.getDrawable(R.drawable.arrow_0),
resources.getDrawable(R.drawable.arrow_20), resources.getDrawable(R.drawable.arrow_40),
resources.getDrawable(R.drawable.arrow_60), resources.getDrawable(R.drawable.arrow_80),
resources.getDrawable(R.drawable.arrow_100), resources.getDrawable(R.drawable.arrow_120),
resources.getDrawable(R.drawable.arrow_140), resources.getDrawable(R.drawable.arrow_160),
resources.getDrawable(R.drawable.arrow_180), resources.getDrawable(R.drawable.arrow_200),
resources.getDrawable(R.drawable.arrow_220), resources.getDrawable(R.drawable.arrow_240),
resources.getDrawable(R.drawable.arrow_260), resources.getDrawable(R.drawable.arrow_280),
resources.getDrawable(R.drawable.arrow_300), resources.getDrawable(R.drawable.arrow_320),
resources.getDrawable(R.drawable.arrow_340) };
arrowWidth = arrows[headingIndex].getIntrinsicWidth();
arrowHeight = arrows[headingIndex].getIntrinsicHeight();
for (Drawable arrow : arrows) {
arrow.setBounds(0, 0, arrowWidth, arrowHeight);
}
statsMarker = resources.getDrawable(R.drawable.yellow_pushpin);
markerWidth = statsMarker.getIntrinsicWidth();
markerHeight = statsMarker.getIntrinsicHeight();
statsMarker.setBounds(0, 0, markerWidth, markerHeight);
waypointMarker = resources.getDrawable(R.drawable.blue_pushpin);
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
startMarker = resources.getDrawable(R.drawable.green_dot);
startMarker.setBounds(0, 0, markerWidth, markerHeight);
endMarker = resources.getDrawable(R.drawable.red_dot);
endMarker.setBounds(0, 0, markerWidth, markerHeight);
errorCirclePaint = TrackPathUtils.getPaint(context, R.color.blue);
errorCirclePaint.setAlpha(127);
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
/**
* Add a location to the map overlay.
* <p>
* NOTE: This method doesn't take ownership of the given location, so it is
* safe to reuse the same location while calling this method.
* Add a location.
*
* @param location the location
*/
public void addLocation(Location location) {
// Queue up in the pendingPoints until it's merged with points.
if (!pendingPoints.offer(new CachedLocation(location))) {
Log.e(TAG, "Unable to add to pendingPoints.");
// Queue up in the pendingLocations until it's merged with locations
if (!pendingLocations.offer(new CachedLocation(location))) {
Log.e(TAG, "Unable to add to pendingLocations.");
}
}
/**
* Adds a segment split to the map overlay.
* Adds a segment split.
*/
public void addSegmentSplit() {
// Queue up in the pendingPoints until it's merged with points.
if (!pendingPoints.offer(new CachedLocation())) {
Log.e(TAG, "Unable to add to pendingPoints");
// Queue up in the pendingLocations until it's merged with locations
if (!pendingLocations.offer(new CachedLocation())) {
Log.e(TAG, "Unable to add to pendingLocations.");
}
}
@@ -223,26 +165,20 @@ public class MapOverlay extends Overlay {
* Clears the locations.
*/
public void clearPoints() {
synchronized (points) {
points.clear();
pendingPoints.clear();
trackPathPainter.clearPath();
lastReferencePoint = null;
lastViewRect = null;
synchronized (locations) {
locations.clear();
pendingLocations.clear();
}
}
/**
* Adds a waypoint to the map overlay.
* Adds a waypoint.
*
* @param waypoint the waypoint
*/
public void addWaypoint(Waypoint waypoint) {
// Note: We don't cache waypoints because it's not worth the effort.
if (waypoint != null && waypoint.getLocation() != null) {
synchronized (waypoints) {
waypoints.add(waypoint);
}
synchronized (waypoints) {
waypoints.add(waypoint);
}
}
@@ -256,327 +192,92 @@ public class MapOverlay extends Overlay {
}
/**
* Sets whether to draw the track or not.
* Sets whether to show the end marker.
*
* @param trackDrawingEnabled true to draw track
* @param show true to show the end marker
*/
public void setTrackDrawingEnabled(boolean trackDrawingEnabled) {
this.trackDrawingEnabled = trackDrawingEnabled;
public void setShowEndMarker(boolean show) {
showEndMarker = show;
}
/**
* Sets whether to draw the end maker or not.
* Updates the track, start and end markers, and waypoints.
*
* @param showEndMarker true to draw end marker
* @param googleMap the google map
* @param paths the paths
* @param reload true to reload all points
*/
public void setShowEndMarker(boolean showEndMarker) {
this.showEndMarker = showEndMarker;
}
/**
* Sets my location.
*
* @param myLocation my location
*/
public void setMyLocation(Location myLocation) {
this.myLocation = myLocation;
}
/**
* Sets the heading.
*
* @param heading the heading
* @return true if the visible heading has changed.
*/
public boolean setHeading(float heading) {
/*
* Use -heading because the arrow images are counter-clockwise rather than
* clockwise.
*/
int index = Math.round(-heading / 360 * 18);
while (index < 0) {
index += 18;
}
while (index > 17) {
index -= 18;
}
if (index != headingIndex) {
headingIndex = index;
return true;
} else {
return false;
}
}
@Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
if (shadow) {
return;
}
// It's safe to keep projection within a single draw operation
Projection projection = getMapProjection(mapView);
if (projection == null) {
Log.w(TAG, "No projection, unable to draw.");
return;
}
if (trackDrawingEnabled) {
// Get the current viewing Rect
Rect viewRect = getMapViewRect(mapView);
// Draw the selected track
drawTrack(canvas, projection, viewRect);
// Draw the start and end markers
drawMarkers(canvas, projection);
// Draw the waypoints
drawWaypoints(canvas, projection);
}
// Draw the current location
drawMyLocation(canvas, projection);
}
@Override
public boolean onTap(GeoPoint geoPoint, MapView mapView) {
if (geoPoint.equals(mapView.getMapCenter())) {
/*
* There is (unfortunately) no good way to determine whether the tap was
* caused by an actual tap on the screen or the track ball. If the
* location is equal to the map center,then it was a track ball press with
* very high likelihood.
*/
return false;
}
final Location tapLocation = LocationUtils.getLocation(geoPoint);
double minDistance = Double.MAX_VALUE;
Waypoint waypoint = null;
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
final Location waypointLocation = waypoints.get(i).getLocation();
if (waypointLocation == null) {
continue;
}
final double distance = waypointLocation.distanceTo(tapLocation);
if (distance < minDistance) {
minDistance = distance;
waypoint = waypoints.get(i);
}
}
}
if (waypoint != null && minDistance < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, waypoint.getId());
context.startActivity(intent);
return true;
}
return super.onTap(geoPoint, mapView);
}
/**
* Gets the points.
*/
@VisibleForTesting
public List<CachedLocation> getPoints() {
return points;
}
/**
* Gets the track path painter.
*/
@VisibleForTesting
public TrackPathPainter getTrackPathPainter() {
return trackPathPainter;
}
/**
* Sets the track path painter.
*
* @param trackPathPainter the track path painter
*/
@VisibleForTesting
public void setTrackPathPainter(TrackPathPainter trackPathPainter) {
this.trackPathPainter = trackPathPainter;
}
/**
* Gets the map view projection.
*
* @param mapView the map view
*/
@VisibleForTesting
protected Projection getMapProjection(MapView mapView) {
return mapView.getProjection();
}
/**
* Gets the map view Rect.
*
* @param mapView the map view
*/
@VisibleForTesting
protected Rect getMapViewRect(MapView mapView) {
int width = mapView.getLongitudeSpan();
int height = mapView.getLatitudeSpan();
int centerX = mapView.getMapCenter().getLongitudeE6();
int centerY = mapView.getMapCenter().getLatitudeE6();
return new Rect(centerX - width / 2, centerY - height / 2, centerX + width / 2, centerY + height
/ 2);
}
/**
* Gets number of locations.
*/
@VisibleForTesting
int getNumLocations() {
synchronized (points) {
return points.size() + pendingPoints.size();
}
}
/**
* Gets number of waypoints.
*/
@VisibleForTesting
int getNumWaypoints() {
synchronized (waypoints) {
return waypoints.size();
}
}
/**
* Draws the track.
*
* @param canvas the canvas
* @param projection the projection
* @param viewRect the view rect
*/
private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) {
boolean draw;
synchronized (points) {
// Merge the pending points with the list of cached locations.
GeoPoint referencePoint = projection.fromPixels(0, 0);
int newPoints = pendingPoints.drainTo(points);
boolean newProjection = !viewRect.equals(lastViewRect)
|| !referencePoint.equals(lastReferencePoint);
// Call updateState first to trigger its side effects.
boolean currentPathValid = !trackPathPainter.updateState() && !newProjection
&& trackPathPainter.hasPath();
if (newPoints == 0 && currentPathValid) {
// No need to update
draw = true;
public void update(GoogleMap googleMap, ArrayList<Polyline> paths, boolean reload) {
synchronized (locations) {
// Merge pendingLocations with locations
int newLocations = pendingLocations.drainTo(locations);
boolean needReload = reload || trackPath.updateState();
if (needReload) {
googleMap.clear();
paths.clear();
trackPath.updatePath(googleMap, paths, 0, locations);
updateStartAndEndMarkers(googleMap);
updateWaypoints(googleMap);
} else {
int numPoints = points.size();
if (numPoints < 2) {
// Not enough points to draw a path
draw = false;
} else if (currentPathValid) {
// Incremental update of the path
draw = true;
trackPathPainter.updatePath(projection, viewRect, numPoints - newPoints, points);
} else {
// Reload the path
draw = true;
trackPathPainter.clearPath();
trackPathPainter.updatePath(projection, viewRect, 0, points);
if (newLocations != 0) {
int numLocations = locations.size();
trackPath.updatePath(googleMap, paths, numLocations - newLocations, locations);
}
}
lastReferencePoint = referencePoint;
lastViewRect = viewRect;
}
if (draw) {
trackPathPainter.drawPath(canvas);
}
}
/**
* Draws the start and end markers.
* Updates the start and end markers.
*
* @param canvas the canvas
* @param projection the projection
* @param googleMap the google map
*/
private void drawMarkers(Canvas canvas, Projection projection) {
int offsetY = (int) (markerHeight * Constants.MARKER_Y_OFFSET_PERCENTAGE);
// Draw the end marker
private void updateStartAndEndMarkers(GoogleMap googleMap) {
// Add the end marker
if (showEndMarker) {
for (int i = points.size() - 1; i >= 0; i--) {
if (points.get(i).valid) {
drawElement(
canvas, projection, points.get(i).geoPoint, endMarker, -markerWidth / 2, -offsetY);
for (int i = locations.size() - 1; i >= 0; i--) {
CachedLocation cachedLocation = locations.get(i);
if (cachedLocation.valid) {
Marker.Options options = Model.newMarkerOptions().position(cachedLocation.getLatLng())
.anchor(MARKER_X_ANCHOR, MARKER_Y_ANCHOR).draggable(false).visible(true)
.icon(BitmapDescriptors.fromResource(R.drawable.red_dot));
googleMap.addMarker(options);
break;
}
}
}
// Draw the start marker
for (int i = 0; i < points.size(); i++) {
if (points.get(i).valid) {
drawElement(
canvas, projection, points.get(i).geoPoint, startMarker, -markerWidth / 2, -offsetY);
// Add the start marker
for (int i = 0; i < locations.size(); i++) {
CachedLocation cachedLocation = locations.get(i);
if (cachedLocation.valid) {
Marker.Options options = Model.newMarkerOptions().position(cachedLocation.getLatLng())
.anchor(MARKER_X_ANCHOR, MARKER_Y_ANCHOR).draggable(false).visible(true)
.icon(BitmapDescriptors.fromResource(R.drawable.green_dot));
googleMap.addMarker(options);
break;
}
}
}
/**
* Draws the waypoints.
* Updates the waypoints.
*
* @param canvas the canvas
* @param projection the projection
* @param googleMap the google map.
*/
private void drawWaypoints(Canvas canvas, Projection projection) {
private void updateWaypoints(GoogleMap googleMap) {
synchronized (waypoints) {
int offsetX = (int) (markerWidth * Constants.WAYPOINT_X_OFFSET_PERCENTAGE);
for (Waypoint waypoint : waypoints) {
Location location = waypoint.getLocation();
Drawable drawable = waypoint.getType() == Waypoint.TYPE_STATISTICS ? statsMarker
: waypointMarker;
drawElement(canvas, projection, LocationUtils.getGeoPoint(location), drawable, -offsetX,
-markerHeight);
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
int drawableId = waypoint.getType() == Waypoint.TYPE_STATISTICS ? R.drawable.yellow_pushpin
: R.drawable.blue_pushpin;
Marker.Options options = Model.newMarkerOptions().position(latLng)
.anchor(WAYPOINT_X_ANCHOR, WAYPOINT_Y_ANCHOR).draggable(false).visible(true)
.icon(BitmapDescriptors.fromResource(drawableId))
.title(String.valueOf(waypoint.getId()));
googleMap.addMarker(options);
}
}
}
/**
* Draws my location.
*
* @param canvas the canvas
* @param projection the projection
*/
private void drawMyLocation(Canvas canvas, Projection projection) {
if (myLocation == null) {
return;
}
Point point = drawElement(canvas, projection, LocationUtils.getGeoPoint(myLocation),
arrows[headingIndex], -(arrowWidth / 2), -(arrowHeight / 2));
// Draw the error circle
float radius = projection.metersToEquatorPixels(myLocation.getAccuracy());
canvas.drawCircle(point.x, point.y, radius, errorCirclePaint);
}
/**
* Draws an element.
*
* @param canvas the canvas
* @param projection the projection
* @param geoPoint the geo point
* @param drawable the drawable
* @param offsetX the x offset
* @param offsetY the y offset
* @return the point of the drawing.
*/
private Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint,
Drawable drawable, int offsetX, int offsetY) {
Point point = new Point();
projection.toPixels(geoPoint, point);
canvas.save();
canvas.translate(point.x + offsetX, point.y + offsetY);
drawable.draw(canvas);
canvas.restore();
return point;
}
}
@@ -28,7 +28,7 @@ import com.google.android.apps.mytracks.fragments.DeleteOneTrackDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteOneTrackDialogFragment.DeleteOneTrackCaller;
import com.google.android.apps.mytracks.fragments.FrequencyDialogFragment;
import com.google.android.apps.mytracks.fragments.InstallEarthDialogFragment;
import com.google.android.apps.mytracks.fragments.MapFragment;
import com.google.android.apps.mytracks.fragments.MyTracksMapFragment;
import com.google.android.apps.mytracks.fragments.StatsFragment;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
@@ -36,7 +36,6 @@ import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.settings.SettingsActivity;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
@@ -63,7 +62,7 @@ import java.util.List;
/**
* An activity to show the track detail.
*
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
@@ -74,19 +73,18 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
private static final String TAG = TrackDetailActivity.class.getSimpleName();
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
// The following are set in onCreate
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub;
private View mapViewContainer;
private TabHost tabHost;
private TabManager tabManager;
private TrackController trackController;
// From intent
private long trackId;
private long markerId;
// Preferences
private long recordingTrackId;
private boolean recordingTrackPaused;
@@ -136,7 +134,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
if (key != null) {
boolean isRecording = trackId == recordingTrackId;
updateMenuItems(isRecording, recordingTrackPaused);
trackController.update(isRecording, recordingTrackPaused);
trackController.update(isRecording, recordingTrackPaused);
}
}
};
@@ -170,25 +168,6 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
}
};
/**
* 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;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -199,18 +178,16 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(
this, bindChangedCallback);
trackDataHub = TrackDataHub.newInstance(this);
mapViewContainer = getLayoutInflater().inflate(R.layout.map, null);
ApiAdapterFactory.getApiAdapter().disableHardwareAccelerated(mapViewContainer);
tabHost = (TabHost) findViewById(android.R.id.tabhost);
tabHost.setup();
tabManager = new TabManager(this, tabHost, R.id.realtabcontent);
TabSpec mapTabSpec = tabHost.newTabSpec(MapFragment.MAP_FRAGMENT_TAG).setIndicator(
TabSpec mapTabSpec = tabHost.newTabSpec(MyTracksMapFragment.MAP_FRAGMENT_TAG).setIndicator(
getString(R.string.track_detail_map_tab), getResources().getDrawable(R.drawable.tab_map));
tabManager.addTab(mapTabSpec, MapFragment.class, null);
tabManager.addTab(mapTabSpec, MyTracksMapFragment.class, null);
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(
getString(R.string.track_detail_chart_tab),
getResources().getDrawable(R.drawable.tab_chart));
@@ -222,7 +199,8 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
if (savedInstanceState != null) {
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
}
trackController = new TrackController(this, trackRecordingServiceConnection, false, recordListener, stopListener);
trackController = new TrackController(
this, trackRecordingServiceConnection, false, recordListener, stopListener);
showMarker();
}
@@ -274,7 +252,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
super.onDestroy();
trackRecordingServiceConnection.unbind();
}
@Override
protected void onHomeSelected() {
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
@@ -427,13 +405,6 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
return trackRecordingServiceConnection;
}
/**
* Gets the map view container.
*/
public View getMapViewContainer() {
return mapViewContainer;
}
/**
* Gets the {@link TrackDataHub}.
*/
@@ -475,10 +446,10 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
*/
private void showMarker() {
if (markerId != -1L) {
MapFragment mapFragmet = (MapFragment) getSupportFragmentManager()
.findFragmentByTag(MapFragment.MAP_FRAGMENT_TAG);
MyTracksMapFragment mapFragmet = (MyTracksMapFragment) getSupportFragmentManager()
.findFragmentByTag(MyTracksMapFragment.MAP_FRAGMENT_TAG);
if (mapFragmet != null) {
tabHost.setCurrentTabByTag(MapFragment.MAP_FRAGMENT_TAG);
tabHost.setCurrentTabByTag(MyTracksMapFragment.MAP_FRAGMENT_TAG);
mapFragmet.showMarker(trackId, markerId);
} else {
Log.e(TAG, "MapFragment is null");
@@ -488,7 +459,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
/**
* Updates the menu items.
*
*
* @param isRecording true if recording
*/
private void updateMenuItems(boolean isRecording, boolean isPaused) {
@@ -516,8 +487,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
String title;
if (isRecording) {
title = getString(
isPaused ? R.string.generic_paused : R.string.generic_recording);
title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
} else {
Track track = MyTracksProviderUtils.Factory.get(this).getTrack(trackId);
title = track != null ? track.getName() : getString(R.string.my_tracks_app_name);
@@ -527,7 +497,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
/**
* Starts the {@link SaveActivity} to save a track.
*
*
* @param trackFileFormat the track file format
*/
private void startSaveActivity(TrackFileFormat trackFileFormat) {
@@ -1,594 +0,0 @@
/*
* 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.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.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.GeoRect;
import com.google.android.apps.mytracks.util.GoogleLocationUtils;
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.Overlay;
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.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import java.util.EnumSet;
import java.util.List;
/**
* A fragment to display map to the user.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class MapFragment extends Fragment
implements View.OnTouchListener, View.OnClickListener, TrackDataListener {
public static final String MAP_FRAGMENT_TAG = "mapFragment";
private static final String CURRENT_LOCATION_KEY = "current_location_key";
private static final String KEEP_MY_LOCATION_VISIBLE_KEY = "keep_my_location_visible_key";
private TrackDataHub trackDataHub;
// True to keep my location visible.
private boolean keepMyLocationVisible;
// True to zoom to my location. Only apply when keepMyLocationVisible is true.
private boolean zoomToMyLocation;
// The track id of the marker to show.
private long markerTrackId;
// The marker id to show
private long markerId;
// The current selected track id. Set in onSelectedTrackChanged.
private long currentSelectedTrackId;
// The current location. Set in onCurrentLocationChanged.
private Location currentLocation;
// UI elements
private View mapViewContainer;
private MapView mapView;
private MapOverlay mapOverlay;
private ImageButton myLocationImageButton;
private TextView messageTextView;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mapViewContainer = ((TrackDetailActivity) getActivity()).getMapViewContainer();
mapView = (MapView) mapViewContainer.findViewById(R.id.map_view);
mapOverlay = new MapOverlay(getActivity());
List<Overlay> overlays = mapView.getOverlays();
overlays.clear();
overlays.add(mapOverlay);
mapView.requestFocus();
mapView.setOnTouchListener(this);
mapView.setBuiltInZoomControls(true);
myLocationImageButton = (ImageButton) mapViewContainer.findViewById(R.id.map_my_location);
myLocationImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showMyLocation();
}
});
messageTextView = (TextView) mapViewContainer.findViewById(R.id.map_message);
ApiAdapterFactory.getApiAdapter().invalidMenu(getActivity());
return mapViewContainer;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
keepMyLocationVisible = savedInstanceState.getBoolean(KEEP_MY_LOCATION_VISIBLE_KEY, false);
currentLocation = (Location) savedInstanceState.getParcelable(CURRENT_LOCATION_KEY);
if (currentLocation != null) {
updateCurrentLocation();
}
}
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEEP_MY_LOCATION_VISIBLE_KEY, keepMyLocationVisible);
if (currentLocation != null) {
outState.putParcelable(CURRENT_LOCATION_KEY, currentLocation);
}
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
}
@Override
public void onDestroyView() {
super.onDestroyView();
ViewGroup parentViewGroup = (ViewGroup) mapViewContainer.getParent();
if (parentViewGroup != null) {
parentViewGroup.removeView(mapViewContainer);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflator) {
menuInflator.inflate(R.menu.map, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
int titleId = R.string.menu_satellite_mode;
if (mapView != null) {
titleId = mapView.isSatellite() ? R.string.menu_map_mode : R.string.menu_satellite_mode;
}
menu.findItem(R.id.map_satellite_mode).setTitle(titleId);
super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
if (mapView != null && menuItem.getItemId() == R.id.map_satellite_mode) {
mapView.setSatellite(!mapView.isSatellite());
return true;
}
return super.onOptionsItemSelected(menuItem);
}
/**
* Shows my location.
*/
private void showMyLocation() {
updateTrackDataHub();
keepMyLocationVisible = true;
zoomToMyLocation = true;
if (currentLocation != null) {
updateCurrentLocation();
}
}
/**
* Shows the marker.
*
* @param id the marker id
*/
private void showMarker(long id) {
MyTracksProviderUtils MyTracksProviderUtils = Factory.get(getActivity());
Waypoint waypoint = MyTracksProviderUtils.getWaypoint(id);
if (waypoint != null && waypoint.getLocation() != null) {
keepMyLocationVisible = false;
GeoPoint center = new GeoPoint((int) (waypoint.getLocation().getLatitude() * 1E6),
(int) (waypoint.getLocation().getLongitude() * 1E6));
mapView.getController().setCenter(center);
mapView.getController().setZoom(mapView.getMaxZoomLevel());
mapView.invalidate();
}
}
/**
* Shows the marker.
*
* @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 (trackId == currentSelectedTrackId) {
showMarker(id);
markerTrackId = -1L;
markerId = -1L;
return;
}
markerTrackId = trackId;
markerId = id;
}
}
@Override
public boolean onTouch(View view, MotionEvent event) {
if (keepMyLocationVisible && event.getAction() == MotionEvent.ACTION_MOVE) {
if (!isVisible(currentLocation)) {
/*
* Only set to false when no longer visible. Thus can keep showing the
* current location with the next location update.
*/
keepMyLocationVisible = false;
}
}
return false;
}
@Override
public void onClick(View v) {
if (v == messageTextView) {
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);
}
}
@Override
public void onLocationStateChanged(LocationState state) {
if (isResumed()) {
final String message;
final boolean isGpsDisabled;
if (!isSelectedTrackRecording()) {
message = null;
isGpsDisabled = false;
} else {
switch (state) {
case DISABLED:
String setting = getString(
GoogleLocationUtils.isAvailable(getActivity()) ? R.string.gps_google_location_settings
: R.string.gps_location_access);
message = getString(R.string.gps_disabled, setting);
isGpsDisabled = true;
currentLocation = null;
updateCurrentLocation();
break;
case NO_FIX:
message = getString(R.string.gps_wait_for_signal);
isGpsDisabled = false;
break;
case BAD_FIX:
message = getString(R.string.gps_wait_for_better_signal);
isGpsDisabled = false;
break;
case GOOD_FIX:
message = null;
isGpsDisabled = false;
break;
default:
throw new IllegalArgumentException("Unexpected state: " + state);
}
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
if (message == null) {
messageTextView.setVisibility(View.GONE);
return;
}
messageTextView.setText(message);
messageTextView.setVisibility(View.VISIBLE);
if (isGpsDisabled) {
Toast.makeText(getActivity(), R.string.gps_not_found, Toast.LENGTH_LONG).show();
// Click to show the location source settings
messageTextView.setOnClickListener(MapFragment.this);
} else {
messageTextView.setOnClickListener(null);
}
}
}
});
}
}
@Override
public void onLocationChanged(Location location) {
if (isResumed()) {
currentLocation = location;
updateCurrentLocation();
}
}
@Override
public void onHeadingChanged(double heading) {
if (isResumed()) {
if (mapOverlay.setHeading((float) heading)) {
mapView.postInvalidate();
}
}
}
@Override
public void onSelectedTrackChanged(final Track track) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
boolean hasTrack = track != null;
mapOverlay.setTrackDrawingEnabled(hasTrack);
if (hasTrack) {
synchronized (this) {
/*
* Synchronize to prevent race condition in changing
* markerTrackId and markerId variables.
*/
currentSelectedTrackId = track.getId();
updateMap(track);
}
mapOverlay.setShowEndMarker(!isSelectedTrackRecording());
}
mapView.invalidate();
}
}
});
}
}
@Override
public void onTrackUpdated(Track track) {
// We don't care.
}
@Override
public void clearTrackPoints() {
if (isResumed()) {
mapOverlay.clearPoints();
}
}
@Override
public void onSampledInTrackPoint(Location location) {
if (isResumed()) {
mapOverlay.addLocation(location);
}
}
@Override
public void onSampledOutTrackPoint(Location location) {
// We don't care.
}
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
mapOverlay.addSegmentSplit();
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
mapView.postInvalidate();
}
}
@Override
public void clearWaypoints() {
if (isResumed()) {
mapOverlay.clearWaypoints();
}
}
@Override
public void onNewWaypoint(Waypoint waypoint) {
if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
// TODO: Optimize locking inside addWaypoint
mapOverlay.addWaypoint(waypoint);
}
}
@Override
public void onNewWaypointsDone() {
if (isResumed()) {
mapView.postInvalidate();
}
}
@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 onMinRecordingDistanceChanged(int minRecordingDistance) {
// We don't care.
return false;
}
/**
* Resumes the trackDataHub. Needs to be synchronized because 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.LOCATION, TrackDataType.HEADING));
}
/**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Updates the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void updateTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.forceUpdateLocation();
}
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Updates the map by either zooming to the requested marker or showing the
* track.
*
* @param track the track
*/
private void updateMap(Track track) {
if (track.getId() == markerTrackId) {
// Show the marker
showMarker(markerId);
markerTrackId = -1L;
markerId = -1L;
} else {
// Show the track
showTrack(track);
}
}
/**
* Returns true if the location is visible.
*
* @param location the location
*/
private boolean isVisible(Location location) {
if (location == null || mapView == null) {
return false;
}
GeoPoint mapCenter = mapView.getMapCenter();
int latitudeSpan = mapView.getLatitudeSpan();
int longitudeSpan = mapView.getLongitudeSpan();
/*
* The bottom of the mapView is obscured by the zoom controls, subtract its
* height from the visible area.
*/
GeoPoint zoomControlBottom = mapView.getProjection().fromPixels(0, mapView.getHeight());
GeoPoint zoomControlTop = mapView.getProjection().fromPixels(
0, mapView.getHeight() - mapView.getZoomButtonsController().getZoomControls().getHeight());
int zoomControlMargin = Math.abs(
zoomControlTop.getLatitudeE6() - zoomControlBottom.getLatitudeE6());
GeoRect geoRect = new GeoRect(mapCenter, latitudeSpan, longitudeSpan);
geoRect.top += zoomControlMargin;
GeoPoint geoPoint = LocationUtils.getGeoPoint(location);
return geoRect.contains(geoPoint);
}
/**
* Updates the current location and centers it if necessary.
*/
private void updateCurrentLocation() {
if (mapOverlay == null || mapView == null) {
return;
}
mapOverlay.setMyLocation(currentLocation);
mapView.postInvalidate();
if (currentLocation != null && keepMyLocationVisible && !isVisible(currentLocation)) {
GeoPoint geoPoint = LocationUtils.getGeoPoint(currentLocation);
MapController mapController = mapView.getController();
mapController.animateTo(geoPoint);
if (zoomToMyLocation) {
// Only zoom in the first time we show the location.
zoomToMyLocation = false;
if (mapView.getZoomLevel() < mapView.getMaxZoomLevel()) {
mapController.setZoom(mapView.getMaxZoomLevel());
}
}
}
}
/**
* Shows the track.
*
* @param track the track
*/
private void showTrack(Track track) {
if (mapView == null || track == null || track.getNumberOfPoints() < 2) {
return;
}
TripStatistics tripStatistics = track.getTripStatistics();
int bottom = tripStatistics.getBottom();
int left = tripStatistics.getLeft();
int latitudeSpanE6 = tripStatistics.getTop() - bottom;
int longitudeSpanE6 = tripStatistics.getRight() - left;
if (latitudeSpanE6 > 0 && latitudeSpanE6 < 180E6 && longitudeSpanE6 > 0
&& longitudeSpanE6 < 360E6) {
keepMyLocationVisible = false;
GeoPoint center = new GeoPoint(bottom + latitudeSpanE6 / 2, left + longitudeSpanE6 / 2);
if (LocationUtils.isValidGeoPoint(center)) {
mapView.getController().setCenter(center);
mapView.getController().zoomToSpan(latitudeSpanE6, longitudeSpanE6);
}
}
}
}
@@ -0,0 +1,667 @@
/*
* 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.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.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdates;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.LocationSource;
import com.google.android.gms.maps.LocationSource.OnLocationChangedListener;
import com.google.android.gms.maps.OnMapInitializeListener;
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.Model;
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.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 OnMapInitializeListener, 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;
// If current zoom level is above this, no need to zoom to DEFAULT_ZOOM_LEVEL
private static final float MIN_ALLOWED_ZOOM_LEVEL = 10f;
// Google's latitude and longitude
private static final double DEFAULT_LATITUDE = 37.423;
private static final double DEFAULT_LONGITUDE = -122.084;
private TrackDataHub trackDataHub;
// Current location
private Location currentLocation;
// True to keep the currentLocation visible
private boolean keepCurrentLocationVisible;
// True to zoom to currentLocation when it is available
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 ImageButton myLocationImageButton;
private TextView messageTextView;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setHasOptionsMenu(true);
ApiAdapterFactory.getApiAdapter().invalidMenu(getActivity());
mapOverlay = new MapOverlay(getActivity());
setOnMapInitializeListener(this);
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = 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(view, 0);
myLocationImageButton = (ImageButton) layout.findViewById(R.id.map_my_location);
myLocationImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
forceUpdateLocation();
keepCurrentLocationVisible = true;
zoomToCurrentLocation = true;
updateCurrentLocation();
}
});
messageTextView = (TextView) layout.findViewById(R.id.map_message);
return layout;
}
@Override
public void onMapInitialize(GoogleMap map) {
googleMap = map;
googleMap.setMyLocationEnabled(true);
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 (keepCurrentLocationVisible && currentLocation != null
&& !isLocationVisible(currentLocation)) {
keepCurrentLocationVisible = false;
zoomToCurrentLocation = false;
}
}
});
LatLng latLng = getDefaultLatLng();
if (latLng != null) {
googleMap.moveCamera(CameraUpdates.newLatLngZoom(latLng, googleMap.getMinZoomLevel()));
}
if (isResumed()) {
resetTrackDataHub();
}
}
@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);
currentLocation = (Location) savedInstanceState.getParcelable(CURRENT_LOCATION_KEY);
updateCurrentLocation();
}
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (currentLocation != null) {
outState.putParcelable(CURRENT_LOCATION_KEY, 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 boolean onOptionsItemSelected(MenuItem menuItem) {
int type = MAP_TYPE_NORMAL;
switch (menuItem.getItemId()) {
case R.id.menu_map:
type = MAP_TYPE_NORMAL;
break;
case R.id.menu_satellite:
type = MAP_TYPE_SATELLITE;
break;
case R.id.menu_terrain:
type = MAP_TYPE_TERRAIN;
break;
default:
return super.onOptionsItemSelected(menuItem);
}
if (googleMap != null) {
googleMap.setMapType(type);
}
return true;
}
@Override
public void onLocationStateChanged(final LocationState state) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isResumed() || googleMap == null) {
return;
}
boolean myLocationEnabled = true;
if (state == LocationState.DISABLED) {
currentLocation = null;
myLocationEnabled = false;
}
googleMap.setMyLocationEnabled(myLocationEnabled);
String message;
boolean isGpsDisabled;
if (!isSelectedTrackRecording()) {
message = null;
isGpsDisabled = false;
} else {
switch (state) {
case DISABLED:
String setting = getString(GoogleLocationUtils.isAvailable(getActivity())
? R.string.gps_google_location_settings
: R.string.gps_location_access);
message = getString(R.string.gps_disabled, setting);
isGpsDisabled = true;
break;
case NO_FIX:
message = getString(R.string.gps_wait_for_signal);
isGpsDisabled = false;
break;
case BAD_FIX:
message = getString(R.string.gps_wait_for_better_signal);
isGpsDisabled = false;
break;
case GOOD_FIX:
message = null;
isGpsDisabled = false;
break;
default:
throw new IllegalArgumentException("Unexpected state: " + state);
}
}
if (message == null) {
messageTextView.setVisibility(View.GONE);
return;
}
messageTextView.setText(message);
messageTextView.setVisibility(View.VISIBLE);
if (isGpsDisabled) {
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);
}
});
} else {
messageTextView.setOnClickListener(null);
}
}
});
}
}
@Override
public void onLocationChanged(final Location location) {
if (isResumed()) {
if (isSelectedTrackRecording() && currentLocation == null && location != null) {
zoomToCurrentLocation = true;
}
currentLocation = location;
updateCurrentLocation();
}
}
@Override
public void onHeadingChanged(double heading) {
// We don't care.
}
@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() {
if (isResumed()) {
mapOverlay.clearPoints();
reloadPaths = true;
}
}
@Override
public void onSampledInTrackPoint(final Location location) {
if (isResumed()) {
mapOverlay.addLocation(location);
}
}
@Override
public void onSampledOutTrackPoint(Location location) {
// We don't care.
}
@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;
}
}
});
}
}
@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 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.LOCATION, TrackDataType.HEADING));
}
/**
* 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;
}
/**
* Resets the trackDataHub. Needs to be synchronized because the trackDataHub
* can be accessed by multiple threads.
*/
private synchronized void resetTrackDataHub() {
pauseTrackDataHub();
resumeTrackDataHub();
}
/**
* 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();
}
/**
* Forces update location. Needs to be synchronized because the trackDataHub
* can be accessed by multiple threads.
*/
private synchronized void forceUpdateLocation() {
if (trackDataHub != null) {
trackDataHub.forceUpdateLocation();
}
}
/**
* 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(CameraUpdates.newLatLngZoom(latLng, getDefaultZoomLevel()));
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;
}
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 = Model.newLatLngBounds(southWest, northEast);
CameraUpdate cameraUpdate = CameraUpdates.newLatLngBounds(bounds, 32);
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 = CameraUpdates.newLatLngZoom(latLng, getDefaultZoomLevel());
googleMap.moveCamera(cameraUpdate);
}
}
});
}
/**
* Gets the default LatLng.
*/
private LatLng getDefaultLatLng() {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(getActivity());
Location location = myTracksProviderUtils.getLastValidTrackPoint();
if (location != null) {
return new LatLng(location.getLatitude(), location.getLongitude());
} else {
return new LatLng(DEFAULT_LATITUDE, DEFAULT_LONGITUDE);
}
}
/**
* Gets the default zoom level. Needs to run on the UI thread.
*/
private float getDefaultZoomLevel() {
if (googleMap == null) {
return DEFAULT_ZOOM_LEVEL;
}
float zoom = googleMap.getCameraPosition().getZoom();
if (zoom <= MIN_ALLOWED_ZOOM_LEVEL) {
return DEFAULT_ZOOM_LEVEL;
}
return zoom;
}
/**
* 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().getLatLngBounds().contains(latLng);
}
}
@@ -1,139 +0,0 @@
/*
* Copyright 2011 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.maps;
import com.google.android.apps.mytracks.ColoredPath;
import com.google.android.apps.mytracks.MapOverlay.CachedLocation;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.Projection;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import java.util.ArrayList;
import java.util.List;
/**
* A path painter that varies the path colors based on fixed speeds or average
* speed margin depending of the TrackPathDescriptor passed to its constructor.
*
* @author Vangelis S.
*/
public class DynamicSpeedTrackPathPainter implements TrackPathPainter {
private final TrackPathDescriptor trackPathDescriptor;
private final Paint slowPaint;
private final Paint mediumPaint;
private final Paint fastPaint;
private final List<ColoredPath> coloredPaths;
public DynamicSpeedTrackPathPainter(Context context, TrackPathDescriptor trackPathDescriptor) {
this.trackPathDescriptor = trackPathDescriptor;
slowPaint = TrackPathUtils.getPaint(context, R.color.slow_path);
mediumPaint = TrackPathUtils.getPaint(context, R.color.normal_path);
fastPaint = TrackPathUtils.getPaint(context, R.color.fast_path);
coloredPaths = new ArrayList<ColoredPath>();
}
@Override
public boolean hasPath() {
return !coloredPaths.isEmpty();
}
@Override
public boolean updateState() {
return trackPathDescriptor.updateState();
}
@Override
public void updatePath(
Projection projection, Rect viewRect, int startIndex, List<CachedLocation> points) {
boolean hasLastPoint = startIndex != 0 && points.get(startIndex -1).isValid();
Point point = new Point();
if (hasLastPoint) {
GeoPoint geoPoint = points.get(startIndex -1).getGeoPoint();
projection.toPixels(geoPoint, point);
}
boolean newSegment = !hasLastPoint;
// Assume if last point exists, it is visible
boolean lastPointVisible = hasLastPoint;
int slowSpeed = trackPathDescriptor.getSlowSpeed();
int normalSpeed = trackPathDescriptor.getNormalSpeed();
for (int i = startIndex; i < points.size(); ++i) {
CachedLocation cachedLocation = points.get(i);
// If not valid, start a new segment
if (!cachedLocation.isValid()) {
newSegment = true;
continue;
}
GeoPoint geoPoint = cachedLocation.getGeoPoint();
// Check if this breaks the existing segment.
boolean pointVisible = viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
if (!pointVisible && !lastPointVisible) {
// This point and the last point are both outside visible area.
newSegment = true;
}
lastPointVisible = pointVisible;
// Either update point or draw a line from the last point
if (newSegment) {
projection.toPixels(geoPoint, point);
newSegment = false;
} else {
ColoredPath coloredPath;
if (cachedLocation.getSpeed() <= slowSpeed) {
coloredPath = new ColoredPath(slowPaint);
} else if (cachedLocation.getSpeed() <= normalSpeed) {
coloredPath = new ColoredPath(mediumPaint);
} else {
coloredPath = new ColoredPath(fastPaint);
}
coloredPath.getPath().moveTo(point.x, point.y);
projection.toPixels(geoPoint, point);
coloredPath.getPath().lineTo(point.x, point.y);
coloredPaths.add(coloredPath);
}
}
}
@Override
public void clearPath() {
coloredPaths.clear();
}
@Override
public void drawPath(Canvas canvas) {
for (int i = 0; i < coloredPaths.size(); i++) {
ColoredPath coloredPath = coloredPaths.get(i);
canvas.drawPath(coloredPath.getPath(), coloredPath.getPathPaint());
}
}
/**
* Gets the colored paths.
*/
@VisibleForTesting
List<ColoredPath> getColoredPaths() {
return coloredPaths;
}
}
@@ -0,0 +1,114 @@
/*
* Copyright 2011 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.maps;
import com.google.android.apps.mytracks.MapOverlay.CachedLocation;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
/**
* A path painter that varies the path colors based on fixed speeds or average
* speed margin depending of the TrackPathDescriptor passed to its constructor.
*
* @author Vangelis S.
*/
public class MultiColorTrackPath implements TrackPath {
private final TrackPathDescriptor trackPathDescriptor;
private final int slowColor;
private final int normalColor;
private final int fastColor;
public MultiColorTrackPath(Context context, TrackPathDescriptor trackPathDescriptor) {
this.trackPathDescriptor = trackPathDescriptor;
slowColor = context.getResources().getColor(R.color.slow_path);
normalColor = context.getResources().getColor(R.color.normal_path);
fastColor = context.getResources().getColor(R.color.fast_path);
}
@Override
public boolean updateState() {
return trackPathDescriptor.updateState();
}
@Override
public void updatePath(GoogleMap googleMap, ArrayList<Polyline> paths, int startIndex,
List<CachedLocation> locations) {
if (googleMap == null) {
return;
}
if (startIndex >= locations.size()) {
return;
}
boolean newSegment = startIndex == 0 || !locations.get(startIndex - 1).isValid();
LatLng lastLatLng = startIndex != 0 ? locations.get(startIndex -1).getLatLng() : null;
ArrayList<LatLng> lastSegmentPoints = new ArrayList<LatLng>();
int lastSegmentColor = paths.size() != 0 ? paths.get(paths.size() - 1).getColor() : slowColor;
boolean useLastPolyline = true;
for (int i = startIndex; i < locations.size(); ++i) {
CachedLocation cachedLocation = locations.get(i);
// If not valid, start a new segment
if (!cachedLocation.isValid()) {
newSegment = true;
lastLatLng = null;
continue;
}
LatLng latLng = cachedLocation.getLatLng();
int color = getColor(cachedLocation.getSpeed());
// Either update point or draw a line from the last point
if (newSegment) {
TrackPathUtils.addPath(googleMap, paths, lastSegmentPoints, lastSegmentColor, useLastPolyline);
useLastPolyline = false;
lastSegmentColor = color;
newSegment = false;
}
if (lastSegmentColor == color) {
lastSegmentPoints.add(latLng);
} else {
TrackPathUtils.addPath(googleMap, paths, lastSegmentPoints, lastSegmentColor, useLastPolyline);
useLastPolyline = false;
if (lastLatLng != null) {
lastSegmentPoints.add(lastLatLng);
}
lastSegmentPoints.add(latLng);
lastSegmentColor = color;
}
lastLatLng = latLng;
}
TrackPathUtils.addPath(googleMap, paths, lastSegmentPoints, lastSegmentColor, useLastPolyline);
}
private int getColor(int speed) {
if (speed <= trackPathDescriptor.getSlowSpeed()) {
return slowColor;
} else if (speed <= trackPathDescriptor.getNormalSpeed()) {
return normalColor;
} else {
return fastColor;
}
}
}
@@ -0,0 +1,78 @@
/*
* Copyright 2012 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.maps;
import com.google.android.apps.mytracks.MapOverlay.CachedLocation;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
/**
* A single color track path.
*
* @author Jimmy Shih
*/
public class SingleColorTrackPath implements TrackPath {
final int color;
public SingleColorTrackPath(Context context) {
color = context.getResources().getColor(R.color.fast_path);
}
@Override
public boolean updateState() {
return false;
}
@Override
public void updatePath(GoogleMap googleMap, ArrayList<Polyline> paths, int startIndex,
List<CachedLocation> locations) {
if (googleMap == null) {
return;
}
if (startIndex >= locations.size()) {
return;
}
boolean newSegment = startIndex == 0 || !locations.get(startIndex - 1).isValid();
ArrayList<LatLng> lastSegmentPoints = new ArrayList<LatLng>();
boolean useLastPolyline = true;
for (int i = startIndex; i < locations.size(); i++) {
CachedLocation cachedLocation = locations.get(i);
// If not valid, start a new segment
if (!cachedLocation.isValid()) {
newSegment = true;
continue;
}
LatLng latLng = cachedLocation.getLatLng();
if (newSegment) {
TrackPathUtils.addPath(googleMap, paths, lastSegmentPoints, color, useLastPolyline);
useLastPolyline = false;
newSegment = false;
}
lastSegmentPoints.add(latLng);
}
TrackPathUtils.addPath(googleMap, paths, lastSegmentPoints, color, useLastPolyline);
}
}
@@ -1,142 +0,0 @@
/*
* Copyright 2011 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.maps;
import com.google.android.apps.mytracks.MapOverlay.CachedLocation;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.Projection;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import java.util.List;
/**
* A path painter that not variates the path colors.
*
* @author Vangelis S.
*/
public class SingleColorTrackPathPainter implements TrackPathPainter {
private final Paint selectedTrackPaint;
private Path path;
public SingleColorTrackPathPainter(Context context) {
selectedTrackPaint = TrackPathUtils.getPaint(context, R.color.red);
}
@Override
public boolean hasPath() {
return path != null;
}
@Override
public boolean updateState() {
return false;
}
@Override
public void updatePath(
Projection projection, Rect viewRect, int startIndex, List<CachedLocation> points) {
if (!hasPath()) {
path = newPath();
}
updatePath(projection, viewRect, startIndex, points, path);
}
@Override
public void clearPath() {
path = null;
}
@Override
public void drawPath(Canvas canvas) {
if (path != null) {
canvas.drawPath(path, selectedTrackPaint);
}
}
/**
* Updates the path.
*
* @param projection the projection
* @param viewRect the view rectangle
* @param startIndex the start index
* @param points the points
* @param pathToUpdate the path to update
*/
@VisibleForTesting
void updatePath(Projection projection, Rect viewRect, int startIndex, List<CachedLocation> points,
Path pathToUpdate) {
pathToUpdate.incReserve(points.size() - startIndex);
boolean hasLastPoint = startIndex != 0 && points.get(startIndex - 1).isValid();
boolean newSegment = !hasLastPoint;
// Assume if last point exists, it is visible
boolean lastPointVisible = hasLastPoint;
Point point = new Point();
for (int i = startIndex; i < points.size(); i++) {
CachedLocation cachedLocation = points.get(i);
// If not valid, start a new segment
if (!cachedLocation.isValid()) {
newSegment = true;
continue;
}
GeoPoint geoPoint = cachedLocation.getGeoPoint();
// Check if this breaks the existing segment.
boolean pointVisible = viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
if (!pointVisible && !lastPointVisible) {
// This point and the last point are both outside visible area.
newSegment = true;
}
lastPointVisible = pointVisible;
// Either update point or draw a line from the last point
projection.toPixels(geoPoint, point);
if (newSegment) {
pathToUpdate.moveTo(point.x, point.y);
newSegment = false;
} else {
pathToUpdate.lineTo(point.x, point.y);
}
}
}
/**
* Creates a new path.
*/
@VisibleForTesting
protected Path newPath() {
return new Path();
}
/**
* Gets the path.
*/
@VisibleForTesting
public Path getPath() {
return path;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2011 Google Inc.
* Copyright 2012 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
@@ -17,50 +17,27 @@
package com.google.android.apps.mytracks.maps;
import com.google.android.apps.mytracks.MapOverlay.CachedLocation;
import com.google.android.maps.Projection;
import android.graphics.Canvas;
import android.graphics.Rect;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.Polyline;
import java.util.ArrayList;
import java.util.List;
/**
* An interface for classes which paint the track path.
* A track path.
*
* @author Vangelis S.
* @author Jimmy Shih
*/
public interface TrackPathPainter {
public interface TrackPath {
/**
* Returns true if has path.
*/
public boolean hasPath();
/**
* Updates state. Returns true if the state is updated.
*/
public boolean updateState();
/**
* Updates the path. Creates a new path if necessary
* Updates the path.
*
* @param projection the projection
* @param viewRect the view rectangle
* @param startIndex the start index
* @param points the points
*/
public void updatePath(
Projection projection, Rect viewRect, int startIndex, List<CachedLocation> points);
/**
* Clears the path.
*/
public void clearPath();
/**
* Draws the path.
*
* @param canvas the canvas
*/
public void drawPath(Canvas canvas);
public void updatePath(GoogleMap googleMap, ArrayList<Polyline> paths, int startIndex,
List<CachedLocation> points);
}
@@ -16,42 +16,37 @@
package com.google.android.apps.mytracks.maps;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.util.Log;
/**
* A factory for {@link TrackPathPainter}.
* A factory for {@link TrackPath}.
*
* @author Vangelis S.
*/
public class TrackPathPainterFactory {
public class TrackPathFactory {
private TrackPathPainterFactory() {}
private TrackPathFactory() {}
/**
* Get a new {@link TrackPathPainter}.
* Get a new {@link TrackPath}.
*
* @param context the context
*/
public static TrackPathPainter getTrackPathPainter(Context context) {
public static TrackPath getTrackPath(Context context) {
String trackColorMode = PreferencesUtils.getString(
context, R.string.track_color_mode_key, PreferencesUtils.TRACK_COLOR_MODE_DEFAULT);
Log.i(TAG, "Creating track path painter of type: " + trackColorMode);
if (context.getString(R.string.settings_map_track_color_mode_dynamic_value)
.equals(trackColorMode)) {
return new DynamicSpeedTrackPathPainter(context, new DynamicSpeedTrackPathDescriptor(
context));
return new MultiColorTrackPath(context, new DynamicSpeedTrackPathDescriptor(context));
} else if (context.getString(R.string.settings_map_track_color_mode_fixed_value)
.equals(trackColorMode)) {
return new DynamicSpeedTrackPathPainter(context, new FixedSpeedTrackPathDescriptor(context));
return new MultiColorTrackPath(context, new FixedSpeedTrackPathDescriptor(context));
} else {
return new SingleColorTrackPathPainter(context);
return new SingleColorTrackPath(context);
}
}
}
@@ -16,8 +16,12 @@
package com.google.android.apps.mytracks.maps;
import android.content.Context;
import android.graphics.Paint;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Model;
import com.google.android.gms.maps.model.Polyline;
import java.util.ArrayList;
/**
* Various utility functions for track path painting.
@@ -29,17 +33,32 @@ public class TrackPathUtils {
private TrackPathUtils() {}
/**
* Gets a paint.
* Add a path.
*
* @param context the context
* @param colorId the color id
* @param googleMap the google map
* @param paths the existing paths
* @param points the path points
* @param color the path color
* @param append true to append to the last path
*/
public static Paint getPaint(Context context, int colorId) {
Paint paint = new Paint();
paint.setColor(context.getResources().getColor(colorId));
paint.setStrokeWidth(3);
paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true);
return paint;
@SuppressWarnings("unchecked")
public static void addPath(GoogleMap googleMap, ArrayList<Polyline> paths,
ArrayList<LatLng> points, int color, boolean append) {
if (points.size() == 0) {
return;
}
if (append && paths.size() != 0) {
Polyline lastPolyline = paths.get(paths.size() - 1);
ArrayList<LatLng> pathPoints = new ArrayList<LatLng>();
pathPoints.addAll(lastPolyline.getPoints());
pathPoints.addAll(points);
lastPolyline.setPoints(pathPoints);
} else {
Polyline.Options options = Model.newPolylineOptions()
.addAll(points).width(5).color(color);
Polyline polyline = googleMap.addPolyline(options);
paths.add(polyline);
}
points.clear();
}
}