diff --git a/MyTracks/res/values/keys.xml b/MyTracks/res/values/keys.xml index 66c4ae207..64afb48f1 100644 --- a/MyTracks/res/values/keys.xml +++ b/MyTracks/res/values/keys.xml @@ -38,7 +38,6 @@ limitations under the License. shareUrlOnly signalSamplingFrequency splitFrequency - debugDrawBounds autoResumeTrackCurrentRetry diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index 3233bd070..386240b6a 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -133,13 +133,4 @@ limitations under the License. android:persistent="false" /> - - - diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java index ce961d7a3..32212e145 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java @@ -51,14 +51,14 @@ import android.os.HandlerThread; import android.provider.Settings; import android.util.Log; import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.SubMenu; import android.view.View; -import android.view.View.OnCreateContextMenuListener; import android.view.Window; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View.OnCreateContextMenuListener; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.RelativeLayout; @@ -140,14 +140,16 @@ public class MyTracksMap extends MapActivity */ private HandlerThread updateTrackThread; - /** Handler for updateTrackThread */ + /** + * Handler for updateTrackThread. + */ private Handler updateTrackHandler; private MyTracksProviderUtils providerUtils; /** * A runnable that updates the track from the provider (looking for points - * added after "lastSeenLocationId". + * added after "lastSeenLocationId"). */ private final Runnable updateTrackRunnable = new Runnable() { @Override @@ -155,6 +157,7 @@ public class MyTracksMap extends MapActivity if (!isATrackSelected()) { return; } + readAllNewTrackPoints(); } }; @@ -168,6 +171,7 @@ public class MyTracksMap extends MapActivity if (!isATrackSelected()) { return; } + mapOverlay.clearPoints(); firstSeenLocationId = -1; lastSeenLocationId = -1; @@ -193,13 +197,10 @@ public class MyTracksMap extends MapActivity cursor = providerUtils.getWaypointsCursor( selectedTrackId, 0, MyTracksConstants.MAX_DISPLAYED_WAYPOINTS_POINTS); - if (cursor != null) { - if (cursor.moveToFirst()) { - do { - Waypoint wpt = providerUtils.createWaypoint(cursor); - mapOverlay.addWaypoint(wpt); - } while (cursor.moveToNext()); - } + if (cursor != null && cursor.moveToFirst()) { + do { + mapOverlay.addWaypoint(providerUtils.createWaypoint(cursor)); + } while (cursor.moveToNext()); } } catch (RuntimeException e) { Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e); @@ -213,9 +214,9 @@ public class MyTracksMap extends MapActivity }; /** - * A runnable intended to be posted to the updateTrackThread after the - * selected track changes. It will post to the ui thread to update the screen - * elements and move the map to show the selected track. + * A runnable intended to be posted to the {@code #updateTrackThread} after + * the selected track changes. It will post to the UI thread to update + * the screen elements and move the map to show the selected track. */ private final Runnable setSelectedTrackRunnable = new Runnable() { @Override @@ -375,12 +376,20 @@ public class MyTracksMap extends MapActivity setSelectedTrack(selectedTrackId); } updateOptionsButton(); - mapOverlay.setDrawBounds(preferences.getBoolean( - getString(R.string.debug_draw_bounds_key), false)); preferences.registerOnSharedPreferenceChangeListener(this); } } + @Override + protected void onDestroy() { + Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy"); + if (updateTrackThread != null) { + updateTrackThread.quit(); + } + + super.onDestroy(); + } + /** * Returns whether there's a track currently selected for display. */ @@ -896,8 +905,7 @@ public class MyTracksMap extends MapActivity @Override public void onSharedPreferenceChanged( - final SharedPreferences sharedPreferences, - final String key) { + final SharedPreferences sharedPreferences, final String key) { if (key != null) { uiHandler.post(new Runnable() { @Override @@ -921,10 +929,6 @@ public class MyTracksMap extends MapActivity mapOverlay.setShowEndMarker(!isRecordingSelected()); mapView.postInvalidate(); } - } else if (key.equals(getString(R.string.debug_draw_bounds_key))) { - mapOverlay.setDrawBounds( - sharedPreferences.getBoolean( - getString(R.string.debug_draw_bounds_key), false)); } } }); @@ -953,8 +957,8 @@ public class MyTracksMap extends MapActivity public void onLocationChanged(Location location) { if (location.getProvider().equals(MyTracksConstants.GPS_PROVIDER)) { // Recalculate the variation if there was a jump in location > 1km: - if (currentLocation == null - || location.distanceTo(currentLocation) > 1000) { + if (currentLocation == null || + location.distanceTo(currentLocation) > 1000) { setVariation(location); } currentLocation = location; @@ -1042,8 +1046,8 @@ public class MyTracksMap extends MapActivity // Now we already have at least one point, calculate the sampling // frequency long numTotalPoints = lastStoredLocationId - firstSeenLocationId; - samplingFrequency = (int) (1 + - numTotalPoints / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS); + samplingFrequency = (int) (1 + numTotalPoints + / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS); } providerUtils.fillLocation(cursor, location); @@ -1055,8 +1059,8 @@ public class MyTracksMap extends MapActivity if (numPoints % samplingFrequency == 0 || (!isRecordingSelected() && locationId == lastStoredLocationId) || !MyTracksUtils.isValidLocation(location)) { - // Only allocate a new location if it is going to be kept around. - mapOverlay.addLocation(new Location(location)); + // No need to allocate a new location (we can safely reuse the existing). + mapOverlay.addLocation(location); } numPoints++; diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java index 47faf8fad..95abbc93b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java @@ -20,20 +20,24 @@ import com.google.android.apps.mytracks.util.MyTracksUtils; 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.maps.mytracks.R; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.Region; import android.graphics.drawable.Drawable; import android.location.Location; import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; /** * A map overlay that displays a "MyLocation" arrow, an error circle, the @@ -43,7 +47,7 @@ import java.util.ArrayList; */ public class MyTracksOverlay extends Overlay { - private final Drawable arrow[] = new Drawable[18]; + private final Drawable[] arrows; private final int arrowWidth, arrowHeight; private final Drawable statsMarker; private final Drawable waypointMarker; @@ -53,68 +57,94 @@ public class MyTracksOverlay extends Overlay { private final Paint selectedTrackPaint; private final Paint errorCirclePaint; private final Context context; - private final ArrayList waypoints = new ArrayList(); - private final ArrayList points = new ArrayList(1024); + private final List waypoints; + private final List points; + private final BlockingQueue pendingPoints; private boolean trackDrawingEnabled; private int lastHeading = 0; private Location myLocation; private boolean showEndMarker = true; - private boolean drawBounds; + // TODO: Remove it completely after completing performance tests. + private boolean alwaysVisible = true; + private GeoPoint lastReferencePoint; + private Rect lastViewRect; + private Path lastPath; + + /** + * Represents a pre-processed {@code Location} to speed up drawing. + * This class is more like a data object and doesn't provide accessors. + */ + private static class CachedLocation { + public final boolean valid; + public final GeoPoint geoPoint; + + public CachedLocation(Location location) { + this.valid = MyTracksUtils.isValidLocation(location); + this.geoPoint = valid ? MyTracksUtils.getGeoPoint(location) : null; + } + }; + public MyTracksOverlay(Context context) { this.context = context; + + this.waypoints = new ArrayList(); + this.points = new ArrayList(1024); + this.pendingPoints = new ArrayBlockingQueue( + MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS, true); // TODO: Can we use a FrameAnimation or similar here rather // than individual resources for each arrow direction? - - arrow[0] = context.getResources().getDrawable(R.drawable.arrow_0); - arrow[1] = context.getResources().getDrawable(R.drawable.arrow_20); - arrow[2] = context.getResources().getDrawable(R.drawable.arrow_40); - arrow[3] = context.getResources().getDrawable(R.drawable.arrow_60); - arrow[4] = context.getResources().getDrawable(R.drawable.arrow_80); - arrow[5] = context.getResources().getDrawable(R.drawable.arrow_100); - arrow[6] = context.getResources().getDrawable(R.drawable.arrow_120); - arrow[7] = context.getResources().getDrawable(R.drawable.arrow_140); - arrow[8] = context.getResources().getDrawable(R.drawable.arrow_160); - arrow[9] = context.getResources().getDrawable(R.drawable.arrow_180); - arrow[10] = context.getResources().getDrawable(R.drawable.arrow_200); - arrow[11] = context.getResources().getDrawable(R.drawable.arrow_220); - arrow[12] = context.getResources().getDrawable(R.drawable.arrow_240); - arrow[13] = context.getResources().getDrawable(R.drawable.arrow_260); - arrow[14] = context.getResources().getDrawable(R.drawable.arrow_280); - arrow[15] = context.getResources().getDrawable(R.drawable.arrow_300); - arrow[16] = context.getResources().getDrawable(R.drawable.arrow_320); - arrow[17] = context.getResources().getDrawable(R.drawable.arrow_340); - arrowWidth = arrow[lastHeading].getIntrinsicWidth(); - arrowHeight = arrow[lastHeading].getIntrinsicHeight(); - for (int i = 0; i <= 17; i++) { - arrow[i].setBounds(0, 0, arrowWidth, arrowHeight); + 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[lastHeading].getIntrinsicWidth(); + arrowHeight = arrows[lastHeading].getIntrinsicHeight(); + for (Drawable arrow : arrows) { + arrow.setBounds(0, 0, arrowWidth, arrowHeight); } - statsMarker = context.getResources().getDrawable(R.drawable.ylw_pushpin); + statsMarker = resources.getDrawable(R.drawable.ylw_pushpin); markerWidth = statsMarker.getIntrinsicWidth(); markerHeight = statsMarker.getIntrinsicHeight(); statsMarker.setBounds(0, 0, markerWidth, markerHeight); - startMarker = context.getResources().getDrawable(R.drawable.green_dot); + startMarker = resources.getDrawable(R.drawable.green_dot); startMarker.setBounds(0, 0, markerWidth, markerHeight); - endMarker = context.getResources().getDrawable(R.drawable.red_dot); + endMarker = resources.getDrawable(R.drawable.red_dot); endMarker.setBounds(0, 0, markerWidth, markerHeight); - waypointMarker = - context.getResources().getDrawable(R.drawable.blue_pushpin); + waypointMarker = resources.getDrawable(R.drawable.blue_pushpin); waypointMarker.setBounds(0, 0, markerWidth, markerHeight); selectedTrackPaint = new Paint(); - selectedTrackPaint.setColor(context.getResources().getColor(R.color.red)); + selectedTrackPaint.setColor(resources.getColor(R.color.red)); selectedTrackPaint.setStrokeWidth(3); selectedTrackPaint.setStyle(Paint.Style.STROKE); selectedTrackPaint.setAntiAlias(true); errorCirclePaint = new Paint(); - errorCirclePaint.setColor(context.getResources().getColor(R.color.blue)); + errorCirclePaint.setColor(resources.getColor(R.color.blue)); errorCirclePaint.setStyle(Paint.Style.STROKE); errorCirclePaint.setStrokeWidth(3); errorCirclePaint.setAlpha(127); @@ -123,36 +153,51 @@ public class MyTracksOverlay extends Overlay { /** * Add a location to the map overlay. + * + * NOTE: This method doesn't take ownership of the given location, so it is + * safe to reuse the same location while calling this method. * - * NOTE: this method takes ownership of this location and may change it. - * - * @param l the location to add + * @param l the location to add. */ public void addLocation(Location l) { - if (l != null) { - synchronized (points) { - points.add(l); - } - } + // Queue up in the pending queue until it's merged with {@code #points}. + pendingPoints.offer(new CachedLocation(l)); } public void addWaypoint(Waypoint wpt) { - waypoints.add(wpt); + // Note: We don't cache waypoints, because it's not worth the effort. + if (wpt != null && wpt.getLocation() != null) { + synchronized (waypoints) { + waypoints.add(wpt); + } + } } - + public int getNumLocations() { synchronized (points) { - return points.size(); + return points.size() + pendingPoints.size(); + } + } + + // Visible for testing. + int getNumWaypoints() { + synchronized (waypoints) { + return waypoints.size(); + } + } + + public void clearPoints() { + synchronized (points) { + points.clear(); + pendingPoints.clear(); + lastPath = null; + lastViewRect = null; } } public void clearWaypoints() { - waypoints.clear(); - } - - public void clearPoints() { - synchronized (points) { - points.clear(); + synchronized (waypoints) { + waypoints.clear(); } } @@ -170,230 +215,175 @@ public class MyTracksOverlay extends Overlay { return; } + // It's safe to keep projection within a single draw operation. + final Projection projection = getMapProjection(mapView); + // Get the current viewing window. if (trackDrawingEnabled) { + Rect viewRect = getMapViewRect(mapView); + // Draw the selected track: - drawTrack(canvas, mapView); + drawTrack(canvas, projection, viewRect); // Draw the waypoints: - drawWaypoints(canvas, mapView); + drawWaypoints(canvas, projection); } // Draw the current location - drawMyLocation(canvas, mapView); + drawMyLocation(canvas, projection); } - - private void drawWaypoints(Canvas canvas, MapView mapView) { - ArrayList currentWaypoints = waypoints; - for (int i = 1; i < currentWaypoints.size(); i++) { - Waypoint wpt = currentWaypoints.get(i); - if (wpt == null) { - continue; - } - Location loc = wpt.getLocation(); - if (loc == null) { - continue; - } - GeoPoint geoPoint = MyTracksUtils.getGeoPoint(loc); - Point pt = new Point(); - mapView.getProjection().toPixels(geoPoint, pt); - canvas.save(); - canvas.translate(pt.x - (markerWidth / 2) + 3, pt.y - (markerHeight)); - if (wpt.getType() == Waypoint.TYPE_STATISTICS) { - statsMarker.draw(canvas); - } else { - waypointMarker.draw(canvas); - } - canvas.restore(); - } + + // Visible for testing. + Projection getMapProjection(MapView mapView) { + return mapView.getProjection(); } - - private void drawMyLocation(Canvas canvas, MapView mapView) { - // Draw the arrow icon: - if (myLocation == null) { - return; - } - - GeoPoint geoPoint = new GeoPoint( - (int) (myLocation.getLatitude() * 1E6), - (int) (myLocation.getLongitude() * 1E6)); - Point pt = new Point(); - mapView.getProjection().toPixels(geoPoint, pt); - canvas.save(); - canvas.translate(pt.x - (arrowWidth / 2), pt.y - (arrowHeight / 2)); - arrow[lastHeading].draw(canvas); - canvas.restore(); - - // Draw the error circle: - float radius = - mapView.getProjection().metersToEquatorPixels(myLocation.getAccuracy()); - canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint); - } - - private void drawTrack(Canvas canvas, MapView mapView) { - Path path; - Point pt = new Point(); - Location lastValidLocation; - int locLon = 0, locLat = 0; - GeoPoint firstGeoPoint = null; - boolean lastLocValid; - - // Get the current viewing window: + + // Visible for testing. + Rect getMapViewRect(MapView mapView) { int w = mapView.getLongitudeSpan(); int h = mapView.getLatitudeSpan(); int cx = mapView.getMapCenter().getLongitudeE6(); int cy = mapView.getMapCenter().getLatitudeE6(); - Rect viewRect = new Rect(cx - w, cy - h, cx + w, cy + h); + return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2); + } + + // Visible for testing. + Path newPath() { + return new Path(); + } + + // Visible for testing. + Path getLastPath() { + return lastPath; + } - // Global bounding box, including points not visible - int allMinLat, allMinLon, allMaxLat, allMaxLon; - + private void drawWaypoints(Canvas canvas, Projection projection) { + synchronized (waypoints) {; + for (Waypoint wpt : waypoints) { + Location loc = wpt.getLocation(); + drawElement(canvas, projection, MyTracksUtils.getGeoPoint(loc), + wpt.getType() == Waypoint.TYPE_STATISTICS ? statsMarker + : waypointMarker, -(markerWidth / 2) + 3, -markerHeight); + } + } + } + + private void drawMyLocation(Canvas canvas, Projection projection) { + // Draw the arrow icon. + if (myLocation == null) { + return; + } + + Point pt = drawElement(canvas, projection, + MyTracksUtils.getGeoPoint(myLocation), arrows[lastHeading], + -(arrowWidth / 2) + 3, -(arrowHeight / 2)); + // Draw the error circle. + float radius = projection.metersToEquatorPixels(myLocation.getAccuracy()); + canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint); + } + + private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) { + Path path; synchronized (points) { - int numPoints = points.size(); - if (numPoints < 2) { - return; - } - - GeoPoint geoPoint; - Location loc; - int minLon, maxLon, minLat, maxLat; - lastValidLocation = points.get(0); - int lastLocLon = allMinLon = allMaxLon = (int) (lastValidLocation.getLongitude() * 1E6); - int lastLocLat = allMinLat = allMaxLat = (int) (lastValidLocation.getLatitude() * 1E6); - lastLocValid = MyTracksUtils.isValidLocation(lastValidLocation); - boolean lastLocVisible = false; - path = new Path(); - - // Loop over track points: - path.incReserve(numPoints); - for (int i = 1; i < numPoints; i++) { - loc = points.get(i); - - boolean locValid = MyTracksUtils.isValidLocation(loc); - boolean locVisible = false; - if (locValid) { - locLon = (int) (loc.getLongitude() * 1E6); - locLat = (int) (loc.getLatitude() * 1E6); - - if (firstGeoPoint == null) { - // Found the starting point - firstGeoPoint = new GeoPoint(locLat, locLon); - } - - // If both the current and previous locations were valid - if (lastLocValid) { - lastValidLocation = loc; - - // Get the bounding box of the segment about to be drawn - if (locLon > lastLocLon) { - minLon = lastLocLon; - maxLon = locLon; - } else { - minLon = locLon; - maxLon = lastLocLon; - } - if (locLat > lastLocLat) { - minLat = lastLocLat; - maxLat = locLat; - } else { - minLat = locLat; - maxLat = lastLocLat; - } - - if (drawBounds) { - allMaxLat = Math.max(allMaxLat, maxLat); - allMinLat = Math.min(allMinLat, minLat); - allMaxLon = Math.max(allMaxLon, maxLon); - allMinLon = Math.min(allMinLon, minLon); - } - - // See if that bounding box intersects the viewable bounding box - // Assume that if it does, the location is visible - locVisible = viewRect.intersects(minLon, minLat, maxLon, maxLat); - if (locVisible) { - // If the previous point wasn't drawn, start at its position - if (!lastLocVisible) { - geoPoint = new GeoPoint(lastLocLat, lastLocLon); - mapView.getProjection().toPixels(geoPoint, pt); - path.moveTo(pt.x, pt.y); - } - - // Draw a line to the new point - geoPoint = new GeoPoint(locLat, locLon); - mapView.getProjection().toPixels(geoPoint, pt); - path.lineTo(pt.x, pt.y); - } - } + // Merge the pending points with the list of cached locations. + final GeoPoint referencePoint = projection.fromPixels(0, 0); + int newPoints = pendingPoints.drainTo(points); + boolean newProjection = !viewRect.equals(lastViewRect) || + !referencePoint.equals(lastReferencePoint); + if (newPoints == 0 && lastPath != null && !newProjection) { + // No need to recreate path (same points and viewing area). + path = lastPath; + } else { + int numPoints = points.size(); + if (numPoints < 2) { + // Not enough points to draw a path. + path = null; + } else if (lastPath != null && !newProjection) { + // Incremental update of the path, without repositioning the view. + path = lastPath; + updatePath(projection, viewRect, path, numPoints - newPoints); + } else { + // The view has changed so we have to start from scratch. + path = newPath(); + path.incReserve(numPoints); + updatePath(projection, viewRect, path, 0); + } + lastPath = path; + } + lastReferencePoint = referencePoint; + lastViewRect = viewRect; + } + if (path != null) { + canvas.drawPath(path, selectedTrackPaint); + } + + // Draw 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, -markerHeight); + break; } - lastLocLon = locLon; - lastLocLat = locLat; - lastLocValid = locValid; - lastLocVisible = locVisible; } } - - canvas.drawPath(path, selectedTrackPaint); - - if (drawBounds) { - drawBoundingBox(canvas, mapView, - allMinLat, allMinLon, allMaxLat, allMaxLon); - } - - // Draw the "End" marker: - if (showEndMarker && lastValidLocation != null) { - canvas.save(); - GeoPoint geoPoint = MyTracksUtils.getGeoPoint(lastValidLocation); - mapView.getProjection().toPixels(geoPoint, pt); - canvas.translate(pt.x - (markerWidth / 2), pt.y - markerHeight); - endMarker.draw(canvas); - canvas.restore(); - } - - // Draw the "Start" marker: - if (firstGeoPoint != null) { - mapView.getProjection().toPixels(firstGeoPoint, pt); - canvas.save(); - canvas.translate(pt.x - (markerWidth / 2), pt.y - markerHeight); - startMarker.draw(canvas); - canvas.restore(); + + // 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, -markerHeight); + break; + } } } - - private void drawBoundingBox(Canvas canvas, MapView mapView, int allMinLat, - int allMinLon, int allMaxLat, int allMaxLon) { - // Transform coordinates - GeoPoint maxPoint = new GeoPoint(allMaxLat, allMaxLon); - GeoPoint minPoint = new GeoPoint(allMinLat, allMinLon); - Point minPt = new Point(); - Point maxPt = new Point(); - mapView.getProjection().toPixels(minPoint, minPt); - mapView.getProjection().toPixels(maxPoint, maxPt); - Rect allBounds = new Rect(minPt.x, minPt.y, maxPt.x, maxPt.y); - - // Prepare a green paint - Paint boundingBoxPaint = new Paint(); - boundingBoxPaint.setColor(context.getResources().getColor(R.color.green)); - boundingBoxPaint.setStrokeWidth(2); - boundingBoxPaint.setStyle(Paint.Style.STROKE); - boundingBoxPaint.setAntiAlias(true); - - // Disable clipping + + private void updatePath(Projection projection, Rect viewRect, Path path, + int startLocationIdx) { + // Whether to start a new segment on new valid and visible point. + boolean newSegment = + startLocationIdx > 0 ? !points.get(startLocationIdx - 1).valid : true; + boolean lastVisible = !newSegment; + final Point pt = new Point(); + // Loop over track points. + for (int i = startLocationIdx; i < points.size(); ++i) { + CachedLocation loc = points.get(i); + // Check if valid, if not then indicate a new segment. + if (!loc.valid) { + newSegment = true; + continue; + } + + final GeoPoint geoPoint = loc.geoPoint; + // Check if this breaks the existing segment. + boolean visible = alwaysVisible || viewRect.contains( + geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6()); + if (!visible && !lastVisible) { + // This is a point outside view not connected to a visible one. + newSegment = true; + } + lastVisible = visible; + + // Either move to beginning of a new segment or continue the old one. + projection.toPixels(geoPoint, pt); + if (newSegment) { + path.moveTo(pt.x, pt.y); + newSegment = false; + } else { + path.lineTo(pt.x, pt.y); + } + } + } + + // Visible for testing. + Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint, + Drawable element, int offsetX, int offsetY) { + Point pt = new Point(); + projection.toPixels(geoPoint, pt); canvas.save(); - canvas.clipRect(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, - Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, - Region.Op.REPLACE); - - // Draw the bounding box - canvas.drawRect(allBounds, boundingBoxPaint); - - // Re-enable clipping + canvas.translate(pt.x + offsetX, pt.y + offsetY); + element.draw(canvas); canvas.restore(); - } - - @Override - public boolean draw(Canvas canvas, MapView mapView, boolean shadow, - long when) { - draw(canvas, mapView, shadow); - return false; + return pt; } /** @@ -436,20 +426,22 @@ public class MyTracksOverlay extends Overlay { final Location tapLocation = MyTracksUtils.getLocation(p); double dmin = Double.MAX_VALUE; Waypoint waypoint = null; - for (int i = 0; i < waypoints.size(); i++) { - final Location waypointLocation = waypoints.get(i).getLocation(); - if (waypointLocation == null) { - continue; - } - final double d = waypointLocation.distanceTo(tapLocation); - if (d < dmin) { - dmin = d; - waypoint = waypoints.get(i); + synchronized (waypoints) { + for (int i = 0; i < waypoints.size(); i++) { + final Location waypointLocation = waypoints.get(i).getLocation(); + if (waypointLocation == null) { + continue; + } + final double d = waypointLocation.distanceTo(tapLocation); + if (d < dmin) { + dmin = d; + waypoint = waypoints.get(i); + } } } - if (waypoint != null - && dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) { + if (waypoint != null && + dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) { Intent intent = new Intent(context, MyTracksWaypointDetails.class); intent.putExtra("waypointid", waypoint.getId()); context.startActivity(intent); @@ -457,8 +449,4 @@ public class MyTracksOverlay extends Overlay { } return super.onTap(p, mapView); } - - public void setDrawBounds(boolean drawBounds) { - this.drawBounds = drawBounds; - } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java new file mode 100644 index 000000000..06049f4d7 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java @@ -0,0 +1,243 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks; + +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.maps.GeoPoint; +import com.google.android.maps.MapView; +import com.google.android.maps.Projection; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.location.Location; +import android.test.AndroidTestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * Tests for the MyTracks map overlay. + * + * @author Bartlomiej Niechwiej + */ +public class MyTracksOverlayTest extends AndroidTestCase { + private Canvas canvas; + private MockMyTracksOverlay myTracksOverlay; + private MapView mockView; + private Projection mockProjection; + + /** + * A mock version of {@code MyTracksOverlay} that does not use + * {@class MapView}. + */ + private class MockMyTracksOverlay extends MyTracksOverlay { + public MockMyTracksOverlay(Context context) { + super(context); + } + @Override + Projection getMapProjection(MapView mapView) { + return mockProjection; + } + @Override + Rect getMapViewRect(MapView mapView) { + return new Rect(0, 0, 100, 100); + } + @Override + Path newPath() { + return new MockPath(); + } + } + + /** + * A mock class that intercepts {@code Path}'s and records calls to + * {@code #moveTo()} and {@code #lineTo()}. + */ + private static class MockPath extends Path { + /** A list of disjoined path segments. */ + public final List> segments = new LinkedList>(); + /** The total number of points in this path. */ + public int totalPoints; + + private List currentSegment; + + @Override + public void lineTo(float x, float y) { + super.lineTo(x, y); + assertNotNull(currentSegment); + currentSegment.add(new PointF(x, y)); + totalPoints++; + } + @Override + public void moveTo(float x, float y) { + super.moveTo(x, y); + segments.add(currentSegment = + new ArrayList(Arrays.asList(new PointF(x, y)))); + totalPoints++; + } + } + + /** + * A mock {@code Projection} that acts as the identity matrix. + */ + private static class MockProjection implements Projection { + @Override + public Point toPixels(GeoPoint in, Point out) { + return out; + } + @Override + public float metersToEquatorPixels(float meters) { + return meters; + } + @Override + public GeoPoint fromPixels(int x, int y) { + return new GeoPoint(y, x); + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + canvas = new Canvas(); + myTracksOverlay = new MockMyTracksOverlay(getContext()); + // Enable drawing. + myTracksOverlay.setTrackDrawingEnabled(true); + mockView = null; + mockProjection = new MockProjection(); + } + + public void testAddLocation() throws Exception { + Location location = new Location("gps"); + location.setLatitude(10); + location.setLongitude(20); + myTracksOverlay.addLocation(location); + assertEquals(1, myTracksOverlay.getNumLocations()); + assertEquals(0, myTracksOverlay.getNumWaypoints()); + + location.setLatitude(20); + location.setLongitude(30); + myTracksOverlay.addLocation(location); + assertEquals(2, myTracksOverlay.getNumLocations()); + assertEquals(0, myTracksOverlay.getNumWaypoints()); + assertNull(myTracksOverlay.getLastPath()); + + // Draw and make sure that we don't lose any point. + myTracksOverlay.draw(canvas, mockView, false); + assertEquals(2, myTracksOverlay.getNumLocations()); + assertEquals(0, myTracksOverlay.getNumWaypoints()); + assertNotNull(myTracksOverlay.getLastPath()); + assertEquals(2, ((MockPath)myTracksOverlay.getLastPath()).totalPoints); + + myTracksOverlay.draw(canvas, mockView, true); + assertEquals(2, myTracksOverlay.getNumLocations()); + assertEquals(0, myTracksOverlay.getNumWaypoints()); + assertNotNull(myTracksOverlay.getLastPath()); + } + + public void testClearPoints() throws Exception { + Location location = new Location("gps"); + location.setLatitude(10); + location.setLongitude(20); + myTracksOverlay.addLocation(location); + assertEquals(1, myTracksOverlay.getNumLocations()); + myTracksOverlay.clearPoints(); + assertEquals(0, myTracksOverlay.getNumLocations()); + + // Same after drawing on canvas. + final int locations = 100; + for (int i = 0; i < locations; ++i) { + myTracksOverlay.addLocation(location); + } + assertEquals(locations, myTracksOverlay.getNumLocations()); + myTracksOverlay.draw(canvas, mockView, false); + myTracksOverlay.draw(canvas, mockView, true); + myTracksOverlay.clearPoints(); + assertEquals(0, myTracksOverlay.getNumLocations()); + } + + public void testAddWaypoint() throws Exception { + Location location = new Location("gps"); + location.setLatitude(10); + location.setLongitude(20); + Waypoint waypoint = new Waypoint(); + waypoint.setLocation(location); + myTracksOverlay.addWaypoint(waypoint); + assertEquals(1, myTracksOverlay.getNumWaypoints()); + assertEquals(0, myTracksOverlay.getNumLocations()); + assertNull(myTracksOverlay.getLastPath()); + + final int waypoints = 10; + for (int i = 0; i < waypoints; ++i) { + waypoint = new Waypoint(); + waypoint.setLocation(location); + myTracksOverlay.addWaypoint(waypoint); + } + assertEquals(1 + waypoints, myTracksOverlay.getNumWaypoints()); + assertEquals(0, myTracksOverlay.getNumLocations()); + assertNull(myTracksOverlay.getLastPath()); + } + + public void testClearWaypoints() throws Exception { + Location location = new Location("gps"); + location.setLatitude(10); + location.setLongitude(20); + Waypoint waypoint = new Waypoint(); + waypoint.setLocation(location); + myTracksOverlay.addWaypoint(waypoint); + assertEquals(1, myTracksOverlay.getNumWaypoints()); + myTracksOverlay.clearWaypoints(); + assertEquals(0, myTracksOverlay.getNumWaypoints()); + } + + public void testDrawing() { + Location location = new Location("gps"); + location.setLatitude(10); + for (int i = 0; i < 40; ++i) { + location.setLongitude(20 + i); + Waypoint waypoint = new Waypoint(); + waypoint.setLocation(location); + myTracksOverlay.addWaypoint(waypoint); + } + for (int i = 0; i < 100; ++i) { + location = new Location("gps"); + location.setLatitude(20 + i / 2); + location.setLongitude(150 - i); + myTracksOverlay.addLocation(location); + } + + // Shadow. + myTracksOverlay.draw(canvas, mockView, true); + // We don't expect to do anything if + assertNull(myTracksOverlay.getLastPath()); + assertEquals(40, myTracksOverlay.getNumWaypoints()); + assertEquals(100, myTracksOverlay.getNumLocations()); + + // No shadow. + myTracksOverlay.draw(canvas, mockView, false); + assertNotNull(myTracksOverlay.getLastPath()); + assertTrue(myTracksOverlay.getLastPath() instanceof MockPath); + MockPath path = (MockPath) myTracksOverlay.getLastPath(); + assertEquals(40, myTracksOverlay.getNumWaypoints()); + assertEquals(100, myTracksOverlay.getNumLocations()); + assertEquals(100, path.totalPoints); + // TODO: Check the points from the path (and the segments). + } +}