/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.Waypoint; 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.mytracks.R; import android.content.Context; import android.content.Intent; 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; /** * A map overlay that displays a "MyLocation" arrow, an error circle, the * currently recording track and optionally a selected track. * * @author Leif Hendrik Wilden */ public class MyTracksOverlay extends Overlay { private final Drawable arrow[] = new Drawable[18]; private final int arrowWidth, arrowHeight; private final Drawable statsMarker; private final Drawable waypointMarker; private final Drawable startMarker; private final Drawable endMarker; private final int markerWidth, markerHeight; 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 boolean trackDrawingEnabled; private int lastHeading = 0; private Location myLocation; private boolean showEndMarker = true; private boolean drawBounds; public MyTracksOverlay(Context context) { this.context = context; // 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); } statsMarker = context.getResources().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.setBounds(0, 0, markerWidth, markerHeight); endMarker = context.getResources().getDrawable(R.drawable.red_dot); endMarker.setBounds(0, 0, markerWidth, markerHeight); waypointMarker = context.getResources().getDrawable(R.drawable.blue_pushpin); waypointMarker.setBounds(0, 0, markerWidth, markerHeight); selectedTrackPaint = new Paint(); selectedTrackPaint.setColor(context.getResources().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.setStyle(Paint.Style.STROKE); errorCirclePaint.setStrokeWidth(3); errorCirclePaint.setAlpha(127); errorCirclePaint.setAntiAlias(true); } /** * Add a location to the map overlay. * * NOTE: this method takes ownership of this location and may change it. * * @param l the location to add */ public void addLocation(Location l) { if (l != null) { synchronized (points) { points.add(l); } } } public void addWaypoint(Waypoint wpt) { waypoints.add(wpt); } public int getNumLocations() { synchronized (points) { return points.size(); } } public void clearWaypoints() { waypoints.clear(); } public void clearPoints() { synchronized (points) { points.clear(); } } public void setTrackDrawingEnabled(boolean trackDrawingEnabled) { this.trackDrawingEnabled = trackDrawingEnabled; } public void setShowEndMarker(boolean showEndMarker) { this.showEndMarker = showEndMarker; } @Override public void draw(Canvas canvas, MapView mapView, boolean shadow) { if (shadow) { return; } if (trackDrawingEnabled) { // Draw the selected track: drawTrack(canvas, mapView); // Draw the waypoints: drawWaypoints(canvas, mapView); } // Draw the current location drawMyLocation(canvas, mapView); } 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(); } } 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: 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); // Global bounding box, including points not visible int allMinLat, allMinLon, allMaxLat, allMaxLon; 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); } } } 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(); } } 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 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.restore(); } @Override public boolean draw(Canvas canvas, MapView mapView, boolean shadow, long when) { draw(canvas, mapView, shadow); return false; } /** * Sets the pointer location (will be drawn on next invalidate). */ public void setMyLocation(Location myLocation) { this.myLocation = myLocation; } /** * Sets the pointer heading in degrees (will be drawn on next invalidate). * * @return true if the visible heading changed (i.e. a redraw of pointer is * potentially necessary) */ public boolean setHeading(float heading) { int newhdg = Math.round(-heading / 360 * 18 + 180); while (newhdg < 0) newhdg += 18; while (newhdg > 17) newhdg -= 18; if (newhdg != lastHeading) { lastHeading = newhdg; return true; } else { return false; } } @Override public boolean onTap(GeoPoint p, MapView mapView) { if (p.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 = 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); } } 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); return true; } return super.onTap(p, mapView); } public void setDrawBounds(boolean drawBounds) { this.drawBounds = drawBounds; } }