forked from upstream-mirrors/OpenTracks
453 lines
15 KiB
Java
453 lines
15 KiB
Java
/*
|
|
* Copyright 2008 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
* use this file except in compliance with the License. You may obtain a copy of
|
|
* the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations under
|
|
* the License.
|
|
*/
|
|
package com.google.android.apps.mytracks;
|
|
|
|
import 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.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.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
|
|
* currently recording track and optionally a selected track.
|
|
*
|
|
* @author Leif Hendrik Wilden
|
|
*/
|
|
public class MyTracksOverlay extends Overlay {
|
|
|
|
private final Drawable[] arrows;
|
|
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 List<Waypoint> waypoints;
|
|
private final List<CachedLocation> points;
|
|
private final BlockingQueue<CachedLocation> pendingPoints;
|
|
|
|
private boolean trackDrawingEnabled;
|
|
private int lastHeading = 0;
|
|
private Location myLocation;
|
|
private boolean showEndMarker = true;
|
|
// 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<Waypoint>();
|
|
this.points = new ArrayList<CachedLocation>(1024);
|
|
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
|
|
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS, true);
|
|
|
|
// TODO: Can we use a FrameAnimation or similar here 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[lastHeading].getIntrinsicWidth();
|
|
arrowHeight = arrows[lastHeading].getIntrinsicHeight();
|
|
for (Drawable arrow : arrows) {
|
|
arrow.setBounds(0, 0, arrowWidth, arrowHeight);
|
|
}
|
|
|
|
statsMarker = resources.getDrawable(R.drawable.ylw_pushpin);
|
|
markerWidth = statsMarker.getIntrinsicWidth();
|
|
markerHeight = statsMarker.getIntrinsicHeight();
|
|
statsMarker.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);
|
|
|
|
waypointMarker = resources.getDrawable(R.drawable.blue_pushpin);
|
|
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
|
|
|
|
selectedTrackPaint = new Paint();
|
|
selectedTrackPaint.setColor(resources.getColor(R.color.red));
|
|
selectedTrackPaint.setStrokeWidth(3);
|
|
selectedTrackPaint.setStyle(Paint.Style.STROKE);
|
|
selectedTrackPaint.setAntiAlias(true);
|
|
|
|
errorCirclePaint = new Paint();
|
|
errorCirclePaint.setColor(resources.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 doesn't take ownership of the given location, so it is
|
|
* safe to reuse the same location while calling this method.
|
|
*
|
|
* @param l the location to add.
|
|
*/
|
|
public void addLocation(Location l) {
|
|
// Queue up in the pending queue until it's merged with {@code #points}.
|
|
pendingPoints.offer(new CachedLocation(l));
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
public int getNumLocations() {
|
|
synchronized (points) {
|
|
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() {
|
|
synchronized (waypoints) {
|
|
waypoints.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;
|
|
}
|
|
|
|
// 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, projection, viewRect);
|
|
|
|
// Draw the waypoints:
|
|
drawWaypoints(canvas, projection);
|
|
}
|
|
|
|
// Draw the current location
|
|
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, 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) {
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 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.translate(pt.x + offsetX, pt.y + offsetY);
|
|
element.draw(canvas);
|
|
canvas.restore();
|
|
return pt;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
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())) {
|
|
Intent intent = new Intent(context, MyTracksWaypointDetails.class);
|
|
intent.putExtra("waypointid", waypoint.getId());
|
|
context.startActivity(intent);
|
|
return true;
|
|
}
|
|
return super.onTap(p, mapView);
|
|
}
|
|
}
|