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/MyTracksOverlay.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java index f4954cac2..a42f8854b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java @@ -20,6 +20,7 @@ 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; @@ -64,15 +65,22 @@ public class MyTracksOverlay extends Overlay { private int lastHeading = 0; private Location myLocation; private boolean showEndMarker = true; - // TODO: Remove it completely. If this is true, drawing is faster by 5%. + // TODO: Remove it completely after completing performance tests. private boolean alwaysVisible = true; + private Rect lastViewRect; + private Path lastPath; + /** - * Represents a pre-processed {@code Location} to speed up drawing. + * Represents a pre-processed {@code Location} to speed up drawing. + * This class is more like a data object and doesn't provide getters/setters. */ private static class CachedLocation { public final boolean valid; public final GeoPoint geoPoint; + // Cached point in the screen's coordinate system. + // TODO: Use it in drawTrack. + public Point point; public CachedLocation(Location location) { this.valid = MyTracksUtils.isValidLocation(location); @@ -84,7 +92,7 @@ public class MyTracksOverlay extends Overlay { this.context = context; this.waypoints = new ArrayList(); - this.points = new ArrayList(256); + this.points = new ArrayList(1024); this.pendingPoints = new ArrayBlockingQueue( MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS, true); @@ -159,6 +167,7 @@ public class MyTracksOverlay extends Overlay { } public void addWaypoint(Waypoint 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); @@ -183,6 +192,8 @@ public class MyTracksOverlay extends Overlay { synchronized (points) { points.clear(); pendingPoints.clear(); + lastPath = null; + lastViewRect = null; } } @@ -206,126 +217,152 @@ 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); + } + + // Visible for testing. + Projection getMapProjection(MapView mapView) { + return mapView.getProjection(); + } + + // 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(); + 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; } - private void drawWaypoints(Canvas canvas, MapView mapView) { + private void drawWaypoints(Canvas canvas, Projection projection) { synchronized (waypoints) {; for (Waypoint wpt : waypoints) { Location loc = wpt.getLocation(); - drawElement(canvas, mapView, MyTracksUtils.getGeoPoint(loc), + drawElement(canvas, projection, MyTracksUtils.getGeoPoint(loc), wpt.getType() == Waypoint.TYPE_STATISTICS ? statsMarker : waypointMarker, -(markerWidth / 2) + 3, -markerHeight); } } } - private void drawMyLocation(Canvas canvas, MapView mapView) { - // Draw the arrow icon: + private void drawMyLocation(Canvas canvas, Projection projection) { + // Draw the arrow icon. if (myLocation == null) { return; } - Point pt = drawElement(canvas, mapView, + Point pt = drawElement(canvas, projection, MyTracksUtils.getGeoPoint(myLocation), arrows[lastHeading], -(arrowWidth / 2) + 3, -(arrowHeight / 2)); - // Draw the error circle: - float radius = - mapView.getProjection().metersToEquatorPixels(myLocation.getAccuracy()); + // Draw the error circle. + float radius = projection.metersToEquatorPixels(myLocation.getAccuracy()); canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint); } - private void drawTrack(Canvas canvas, MapView mapView) { + private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) { + Path path; + final Point pt = new Point(); GeoPoint firstGeoPoint = null; GeoPoint lastGeoPoint = null; - - // Get the current viewing window. - Rect viewRect = null; - if (!alwaysVisible) { - int w = mapView.getLongitudeSpan(); - int h = mapView.getLatitudeSpan(); - int cx = mapView.getMapCenter().getLongitudeE6(); - int cy = mapView.getMapCenter().getLatitudeE6(); - viewRect = new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2); - } - - final Path path = new Path(); - final Point pt = new Point(); synchronized (points) { // Merge the pending points with the list of cached locations. - pendingPoints.drainTo(points); - - int numPoints = points.size(); - if (numPoints < 2) { - return; - } - path.incReserve(numPoints); - - // Whether to start a new segment on new valid and visible point. - boolean newSegment = true; - boolean lastVisible = false; - // Loop over track points: - for (CachedLocation loc : points) { - // Check if valid, if not then indicate a new segment. - if (!loc.valid) { - newSegment = true; - continue; + if (pendingPoints.drainTo(points) == 0 && lastPath != null && + viewRect.equals(lastViewRect)) { + // No need to recreate path (same points and viewing area). + path = lastPath; + } else { + // Regenerate the whole path. + int numPoints = points.size(); + if (numPoints < 2) { + return; } - - final GeoPoint geoPoint = loc.geoPoint; - if (firstGeoPoint == null) { - // Found the starting point. - firstGeoPoint = geoPoint; - } - lastGeoPoint = geoPoint; - - // Check if break the existing segment. - boolean visible = alwaysVisible || viewRect.contains( - geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6()); - if (!visible && !lastVisible) { - // So 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. - mapView.getProjection().toPixels(geoPoint, pt); - if (newSegment) { - path.moveTo(pt.x, pt.y); - newSegment = false; - } else { - path.lineTo(pt.x, pt.y); + path = newPath(); + path.incReserve(numPoints); + + // Whether to start a new segment on new valid and visible point. + boolean newSegment = true; + boolean lastVisible = false; + // Loop over track points: + for (CachedLocation loc : points) { + // Check if valid, if not then indicate a new segment. + if (!loc.valid) { + newSegment = true; + continue; + } + + final GeoPoint geoPoint = loc.geoPoint; + if (firstGeoPoint == null) { + // Found the starting point. + firstGeoPoint = geoPoint; + } + lastGeoPoint = 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); + } } + lastPath = path; + lastViewRect = viewRect; } } canvas.drawPath(path, selectedTrackPaint); // Draw the "End" marker. if (showEndMarker && lastGeoPoint != null) { - drawElement(canvas, mapView, lastGeoPoint, endMarker, -markerWidth / 2, + drawElement(canvas, projection, lastGeoPoint, endMarker, -markerWidth / 2, -markerHeight); } // Draw the "Start" marker: if (firstGeoPoint != null) { - drawElement(canvas, mapView, firstGeoPoint, startMarker, -markerWidth / 2, - -markerHeight); + drawElement(canvas, projection, firstGeoPoint, + startMarker, -markerWidth / 2, -markerHeight); } } - private Point drawElement(Canvas canvas, MapView mapView, GeoPoint geoPoint, + // Visible for testing. + Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint, Drawable element, int offsetX, int offsetY) { Point pt = new Point(); - mapView.getProjection().toPixels(geoPoint, pt); + projection.toPixels(geoPoint, pt); canvas.save(); canvas.translate(pt.x + offsetX, pt.y + offsetY); element.draw(canvas); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java index 794319fb3..04b19fb55 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java @@ -16,12 +16,24 @@ 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. * @@ -29,16 +41,71 @@ import android.test.AndroidTestCase; */ public class MyTracksOverlayTest extends AndroidTestCase { private Canvas canvas; - private MyTracksOverlay myTracksOverlay; + private MockMyTracksOverlay myTracksOverlay; private MapView mockView; + private Projection mockProjection; + + 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(); + } + } + + private static class MockPath extends Path { + public final List> segments = new LinkedList>(); + 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++; + } + } @Override protected void setUp() throws Exception { super.setUp(); canvas = new Canvas(); - myTracksOverlay = new MyTracksOverlay(getContext()); - // TODO: add real mock view and enable track drawing. + myTracksOverlay = new MockMyTracksOverlay(getContext()); + // Enable drawing. + myTracksOverlay.setTrackDrawingEnabled(true); mockView = null; + mockProjection = new 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); + } + }; } public void testAddLocation() throws Exception { @@ -47,17 +114,26 @@ public class MyTracksOverlayTest extends AndroidTestCase { 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 { @@ -89,6 +165,8 @@ public class MyTracksOverlayTest extends AndroidTestCase { 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) { @@ -97,6 +175,8 @@ public class MyTracksOverlayTest extends AndroidTestCase { myTracksOverlay.addWaypoint(waypoint); } assertEquals(1 + waypoints, myTracksOverlay.getNumWaypoints()); + assertEquals(0, myTracksOverlay.getNumLocations()); + assertNull(myTracksOverlay.getLastPath()); } public void testClearWaypoints() throws Exception { @@ -122,12 +202,26 @@ public class MyTracksOverlayTest extends AndroidTestCase { } for (int i = 0; i < 100; ++i) { location = new Location("gps"); - location.setLatitude(50 + i); + location.setLatitude(20 + i / 2); location.setLongitude(150 - i); myTracksOverlay.addLocation(location); } - myTracksOverlay.draw(canvas, mockView, false); + // 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). } }