I'm using an Overlay to mark areas on Google Maps by drawing a shape of ten thousands of GeoPoints I get from any source. This works and looks like this:
#Override
public void draw(android.graphics.Canvas canvas, MapView mapView, boolean shadow) {
super.draw(canvas, mapView, false);
Projection projection = mapView.getProjection();
List<Zone> zones = ApplicationContext.getZones();
path.rewind();
for (Zone zone : zones) {
paint.setDither(true);
paint.setStyle(Style.FILL);
paint.setAlpha(40);
MultiPolygon multiPolygon = zone.getMultiPolygon();
List<Polygon> polygons = multiPolygon.getPolygons();
for (Polygon polygon : polygons) {
for (List<Coordinate> coordinates : polygon.getCoordinates()) {
for (int i = 0; i < coordinates.size(); i++) {
Point p = new Point();
projection.toPixels(new GeoPoint((int)(coordinates.get(i).getLatitude() * 1E6), (int)(coordinates.get(i).getLongitude() * 1E6)), p);
if (i == 0) {
path.moveTo(p.x, p.y);
}
else {
path.lineTo(p.x, p.y);
}
}
}
}
}
canvas.drawPath(path, paint);
}
The problem is that this is very resource consuming. Every time one scrolls or moves the map on MapView, the path has to be calculated over and over again, because the pixel coordinates have been changed. The drawn area could become so big that the scrolling on the MapView is so slow that it is functional unusable.
My ideas are
to somehow cache the "shape" the path generates and just redraw it
when the zoom level changes on the MapView.
to somehow draw the painting on an "on the fly"-Bitmap to use it as Overlay (maybe as ItemizedOverlay), listen for MapView scrolling and move the bitmap by the scrolled distance.
I'm not sure if there are better methods.
Any ideas how I could solve this problem?
(I'm using Google Maps API 1 and can't change).
Before resorting to trying to figure out how to match the map's movement, there are some optimizations to your current code that will probably yield significant savings. In particular, these two lines inside your inner loop is executed the most times, but fairly expensive to execute (two memory allocations, floating point multiplies, and four method calls).
Point p = new Point();
projection.toPixels(new GeoPoint((int)(coordinates.get(i).getLatitude() * 1E6), (int)(coordinates.get(i).getLongitude() * 1E6)), p);
First, you only ever need one Point object, so avoid allocating it in your loop. Move it to just below your path.rewind();
Second, if you pre-computed your coordinates as GeoPoints instead of computing them each time, you would save a lot of processing in your draw routine. You can also get rid of that if statement with a little work. Assuming you preconvert your list of coordinate to a list of GeoPoint, and make it available through polygon.getGeoCoordinates(), you could end up with your inner loops looking like -
for (List<GeoPoint> geoCoordinates : polygon.getGeoCoordinates()) {
projection.toPixels(geoCoordinates.get(0),p);
path.moveTo(p.x, p.y); // move to first spot
final List<GeoPoint> lineToList = geoCoordinates.sublist(1,geoCoordinates.size()); // A list of all the other points
for(GeoPoint gp : lineToList) {
projection.toPixels(gp, p);
path.lineTo(p.x, p.y);
}
}
And that will run a lot faster than what you were doing before.
After tinkering around in the last days I found a possible solution (and I don't think there is a better one) to not draw the path over and over again but move it to the current position.
The difficult part was to figure out how to cache the drawn shape to not calculate it over and over again. This can be done by using a Matrix. With this Matrix (I imagine this as some kind of "template") you can manipulate the points coordinates inside the path. The first time (when someone starts moving the Map) I draw the area as usual. When it tries to calculate it the second time or more, I don't redraw the shape but I manipulate the path by calculating the "delta" from the current point to the last point. I know what the current point is, because I always map the original GeoPoint (which always stays the same) to the point which results from the current projection. The "delta" needs to be set as Matrix. After that I transform the path by using this new Matrix. The result is really very fast. The scrolling of the Map is as fast as without using an Overlay.
This looks like this (this is no production code, and it cannot deal with zooming yet, but it shows the principle I use as basis for my optimizations):
public class DistrictOverlay extends Overlay {
// private final static String TAG = DistrictOverlay.class.getSimpleName();
private Paint paint = new Paint();
private Path path = new Path();
private boolean alreadyDrawn = false;
private GeoPoint origGeoPoint;
Point p = new Point();
Point lastPoint = new Point();
#Override
public void draw(android.graphics.Canvas canvas, MapView mapView, boolean shadow) {
super.draw(canvas, mapView, false);
Projection projection = mapView.getProjection();
List<Zone> zones = ApplicationContext.getZones();
if (!alreadyDrawn) {
path.rewind();
for (Zone zone : zones) {
if (!zone.getZoneId().equals(MenuContext.getChosenZoneId())) {
continue;
}
String dateString = zone.getEffectiveFrom().trim().replace("CEST", "").replace("GMT", "").replace("CET", "").replace("MESZ", "");
if (DateUtil.isBeforeCurrentDate(dateString)) {
paint.setColor(Color.RED);
} else {
paint.setColor(Color.GREEN);
}
paint.setDither(true);
paint.setStyle(Style.FILL);
paint.setAlpha(40);
MultiPolygon multiPolygon = zone.getMultiPolygon();
List<Polygon> polygons = multiPolygon.getPolygons();
for (Polygon polygon : polygons) {
for (List<GeoPoint> geoPoints : polygon.getGeoPoints()) {
projection.toPixels(geoPoints.get(0), p);
path.moveTo(p.x, p.y);
origGeoPoint = new GeoPoint(geoPoints.get(0).getLatitudeE6(), geoPoints.get(0).getLongitudeE6());
lastPoint = new Point(p.x, p.y);
final List<GeoPoint> pathAsList = geoPoints.subList(1, geoPoints.size());
for (GeoPoint geoPoint : pathAsList) {
projection.toPixels(geoPoint, p);
path.lineTo(p.x, p.y);
}
}
}
}
}
else {
projection.toPixels(origGeoPoint, p);
Matrix translateMatrix = new Matrix();
translateMatrix.setTranslate(p.x - lastPoint.x, p.y - lastPoint.y);
path.transform(translateMatrix);
lastPoint = new Point(p.x, p.y);
}
canvas.drawPath(path, paint);
if (!path.isEmpty()) {
alreadyDrawn = true;
}
}
#Override
public boolean onTap(GeoPoint p, MapView mapView) {
return true;
}
}
Related
I'm developing an Android aplication that has to show some "places of interest" on a mapview, along with the device current location. This works well.
Also, in my app, the user could "tap" a "place of interest" marker and the aplication would have to draw a route to that marker.
I used Google Directions api to get the route, along with a polyline decoder to get the GeoPoints between the user and the place. For my testing route, google gives me about 200 different GeoPoints.
So, I have a class like this to add those GeoPoints:
public class RouteOverlay extends Overlay {
private GeoPoint gp1;
private GeoPoint gp2;
private int color;
public RouteOverlay(GeoPoint gp1, GeoPoint gp2, int color) {
this.gp1 = gp1;
this.gp2 = gp2;
this.color = color;
}
#Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
Projection projection = mapView.getProjection();
Paint paint = new Paint();
Point point = new Point();
projection.toPixels(gp1, point);
paint.setColor(color);
Point point2 = new Point();
projection.toPixels(gp2, point2);
paint.setStrokeWidth(5);
paint.setAlpha(120);
canvas.drawLine(point.x, point.y, point2.x, point2.y, paint);
super.draw(canvas, mapView, shadow);
}
}
what I do to draw the route is the following:
1) Detect the onClick event to a marker in the map.
2) From that event, I create a new thread, where I make the call to the Google API.
3) Once I have the result, I parse/convert it in a GeoPoint list.
4) Then I call my drawPath method:
private void drawPath(List<GeoPoint> geoPoints, int color) {
mapOverlays.clear();
mapOverlays.add(myLocationOverlay);
mapOverlays.add(itemizedoverlay);
for (int i = 1; i < geoPoints.size(); i++) {
mapOverlays.add(new RouteOverlay(geoPoints.get(i - 1), geoPoints.get(i), color));
}
mapView.postInvalidate();
5) Finally, I return to the UI thread.
This method clears the map overlay list (mapOverlays). Then, adds to the list the current location and the "places of interest" overlays. And, finally, adds the route overlays.
The problem is that, suddenly, works veeery slow and finally crashes. But there is no message in the LogCat. So, I thought that 30 overlays + 1 + more than 200 for the route are too much for the phone to handle. But the tutorials I've seen do it this way so...
Can someone tell me if I do anything wrong?
Thanks in advance.
I figured out what I was doing wrong.
When I called the drawPath function, after obtaining the List of GeoPoints, I made a loop to check the coordinates of each point. Something like
for (int i = 0; i < geoList.size(); i++){
Log.i("GEOPOINT " + i, geoList.get(i).toString());
drawpath(geoList, Color.BLUE);
}
The drawPath function got called N times. So the device crashed. My fault.
Programming at 2:00 am is not good for the code!
I want to create a functionality like Google Maps. I want to update my location marker whenever a person start walking/driving. I am thinking of using onLocationChange method of LocationListner but I am not sure. I also like to draw the path with updating location on the Google Maps.
All suggestions are most welcome.
ItemizedOverlay:-
public class LocationOverlay extends ItemizedOverlay<OverlayItem>{
private List<OverlayItem> myOverlays;
Path path;
static int cnt = -1;
public LocationOverlay(Drawable defaultMarker) {
super(boundCenterBottom(defaultMarker));
System.out.println("Normal...");
myOverlays = new ArrayList<OverlayItem>();
}
public void addOverlay(OverlayItem overlay) {
System.out.println("Add Overlay called");
myOverlays.add(overlay);
populate();
cnt++;
}
#Override
protected OverlayItem createItem(int i) {
return myOverlays.get(i);
}
public void removeItem(int i) {
myOverlays.remove(i);
populate();
}
#Override
public int size() {
return myOverlays.size();
}
#Override
public void draw(Canvas canvas, final MapView mapView, boolean shadow) {
if (shadow) {
paint.setDither(true);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(4);
path = new Path();
Point point1 = new Point();
Projection projection = mapView.getProjection();
if (myOverlays.size() > 1) {
System.out.println(">>>>>>>>>>>");
for (int i = 0, j=i; i < myOverlays.size(); i++) {
Point point1 = new Point();
Point point2 = new Point();
projection.toPixels(myOverlays.get(i).getPoint(), point1);
System.out.println(">>>>>>>>>>>");
projection.toPixels(myOverlays.get(j++).getPoint(), point2);
System.out.println("Point == " + j);
path.moveTo(point2.x, point2.y);
path.lineTo(point1.x, point1.y);
canvas.drawPath(path, paint);
super.draw(canvas, mapView, shadow);
}
}
}
canvas.drawPath(path, paint);
super.draw(canvas, mapView, shadow);
}
}
NOTE - draw method gets called multiple time for single locationChanged.
EDIT :-
I am able to draw the route whenever there is any location changed event fire but I am facing a small problem there. Its also drawing path from root every time. I want that from last place it should start drawing the path (i.e connecting from last to next location). I am attaching one image for better understanding.
From the image you can see that the starting point is also connected to last point. I don't want this. Any idea on this.
Current location can be easily drawn with MyLocationOverlay. What you need to do - is to add this overlay to your MapView. Something like:
mapView.addOverlay(new MyLocationOverlay(context, mapView));
The default location dot will be drawn automatically and will be updated as soon as you move
With route it is a bit trickier, but still there is no rocket science in there. What you need to do - is to extend Overlay class and implement draw() logic. You need to keep track of GeoPoints you travelled and project them on a map (in your overlay's draw() you can ask your mapView for a Projection instance - getProjection(). You can use this projection to map GeoPoints to your map coordinates).
As soon as you have list of GeoPoints - you can create a Path and draw this Path on a Canvas
The simplified example how to draw path on a map (there are some custom classes I use in my project, but you should get an idea):
Path path;
#Override
public void draw(Canvas canvas, final MapView mapView, boolean shadow)
{
Projection proj = mapView.getProjection();
updatePath(mapView,proj);
canvas.drawPath(path, mPaint);
}
protected void updatePath(final MapView mapView, Projection proj)
{
path = new Path();
proj.toPixels(mPoints.get(0).point, p);
path.moveTo(p.x, p.y);
for(RouteEntity.RoutePoint rp : mPoints)
{
proj.toPixels(rp.point, mPoint);
path.lineTo(mPoint.x, mPoint.y);
}
}
So I have an custom overlay item that I have written to fill in a transparent blue overlay based around an array of geo points
#Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
Projection projection = mapView.getProjection();
Paint fill = new Paint();
fill.setColor(Color.BLUE);
fill.setAlpha(50);
fill.setStyle(Paint.Style.FILL_AND_STROKE);
Path path = new Path();
Point firstPoint = new Point();
projection.toPixels(geoPoints.get(0), firstPoint);
path.moveTo(firstPoint.x, firstPoint.y);
for (int i = 1; i < geoPoints.size(); ++i) {
Point nextPoint = new Point();
projection.toPixels(geoPoints.get(i), nextPoint);
path.lineTo(nextPoint.x, nextPoint.y);
}
path.lineTo(firstPoint.x, firstPoint.y);
path.setLastPoint(firstPoint.x, firstPoint.y);
canvas.drawPath(path, fill);
super.draw(canvas, mapView, shadow);
}
What I need is a way to get the center point of this overlay so I can place a marker on it,
anyone have any ideas?
although i am not familiar with android framework, i assume you writing in java and using some kind of google maps api. But i do familiar with graphics and geo development. My suggestion to you firsst of all to check whether the standard api has some kind of
getBounds(path) that returns to you RectangularBounds object or similar. Then from rectangular bounds you can ask for bounds.getCenter() which returns the center of bounds as geo point or other metric. If you use pixels just convert the geopoint like you did...
If getBounds doesn't exists in api (what is hard to believe), just implement a simple interface , you can find a lot of examples on the net.
simple pseudo code for finding the bounds of a geo shape for geo points, if you need pixels use x,y respectively:
bounds = { topLeft: new GeoPoint(path[0]), bottomRight: new GeoPoint(path[0])};
for( point in path ){
bounds.topLeft.lat = max( bounds.topLeft.lat,point.lat );
bounds.topLeft.lng = min( bounds.topLeft.lng,point.lng );
bounds.bottomRight.lat = min( bounds.bottomRight.lat,point.lat );
bounds.bottomRight.lng = max( bounds.bottomRight.lng,point.lng );
}
bounds.getCenter(){
return new GeoPoint(rectangle center point);
// i am sure you will able to manage the code here )))
}
hope this will help
So I have a MapView with a lot of markers, most of which are concentrated in mile wide clusters. When zoomed the markers overlap and appear to only be one. What I want to achieve is at a certain zoom level replace the overlapping markers with a group marker that will display the density of markers and onClick will zoom to display all markers inside. I know I can do this with brute force distance measurements but there must be a more efficient way. Anyone have any solution or smart algorithms on how I can achieve this?
Um... assuming the markers are not grouped, layered or anything: why - before showing them - don't you create a grid of certain density and simply bin the markers into the cells of your grid?
If you then count that several markers fall into the same bin (grid cell) - you can group them. If you need slightly more clever grouping, you might also check the neighbouring cells.
Maybe it sounds a bit primitive but:
No n^2 algorithms
No assumption about ordering of the input
No need to additionally process markers which are not going to be shown
The code for the grid:
Note - I come from the C++ world (got here through [algorithm] tag) so I'll stick to the pseudo-C++. I do not know the API of the mapview. But I would be surprised if this couldn't be efficiently translated into whatever language/library you are using.
Input:
- list of markers
- the rectangle viewing window in world coordinates (section of world we are currently looking at)
In the simplest form, it would look something like this:
void draw(MarkerList mlist, View v) {
//binning:
list<Marker> grid[densityX][densityY]; //2D array with some configurable, fixed density
foreach(Marker m in mlist) {
if (m.within(v)) {
int2 binIdx;
binIdx.x=floor(densityX*(m.coord.x-v.x1)/(v.x2-v.x1));
binIdx.y=floor(densityY*(m.coord.y-v.y1)/(v.y2-v.y1));
grid[binIdx.x][binIdx.y].push(m); //just push the reference
}
//drawing:
for (int i=0; i<densityX; ++i)
for (int j=0; j<densityY; ++j) {
if (grid[i][j].size()>N) {
GroupMarker g;
g.add(grid[i][j]); //process the list of markers belonging to this cell
g.draw();
} else {
foreach (Marker m in grid[i][j])
m.draw()
}
}
}
The problem that might appear is that an unwanted grid split may appear within some clustered group, forming two GroupMarkers. To counter that, you may want to consider not just one grid cell, but also its neighbors in the "\drawing" section, and - if grouped - mark neighboring cells as visited.
The following pragmatic solution based on pixel distance really worked best for me:
http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps
I converted Cygnus X1's answer to Java. Put this method in your custom Overlay and modify drawSingle() and drawGroup() to suit your needs. You improve performance too, like converting the ArrayLists to primitive arrays.
#Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
// binning:
int densityX = 10;
int densityY = 10;
// 2D array with some configurable, fixed density
List<List<List<OverlayItem>>> grid = new ArrayList<List<List<OverlayItem>>>(
densityX);
for(int i = 0; i<densityX; i++){
ArrayList<List<OverlayItem>> column = new ArrayList<List<OverlayItem>>(densityY);
for(int j = 0; j < densityY; j++){
column.add(new ArrayList<OverlayItem>());
}
grid.add(column);
}
for (OverlayItem m : mOverlays) {
int binX;
int binY;
Projection proj = mapView.getProjection();
Point p = proj.toPixels(m.getPoint(), null);
if (isWithin(p, mapView)) {
double fractionX = ((double)p.x / (double)mapView.getWidth());
binX = (int) (Math.floor(densityX * fractionX));
double fractionY = ((double)p.y / (double)mapView.getHeight());
binY = (int) (Math
.floor(densityX * fractionY));
// Log.w("PointClusterer absolute", p.x+ ", "+p.y);
// Log.w("PointClusterer relative", fractionX+ ", "+fractionY);
// Log.w("PointClusterer portion", "Marker is in portion: " + binX
// + ", " + binY);
grid.get(binX).get(binY).add(m); // just push the reference
}
}
// drawing:
for (int i = 0; i < densityX; i++) {
for (int j = 0; j < densityY; j++) {
List<OverlayItem> markerList = grid.get(i).get(j);
if (markerList.size() > 1) {
drawGroup(canvas, mapView, markerList);
} else {
// draw single marker
drawSingle(canvas, mapView, markerList);
}
}
}
}
private void drawGroup(Canvas canvas, MapView mapView,
List<OverlayItem> markerList) {
GeoPoint point = markerList.get(0).getPoint();
Point ptScreenCoord = new Point();
mapView.getProjection().toPixels(point, ptScreenCoord);
Paint paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(30);
paint.setAntiAlias(true);
paint.setARGB(150, 0, 0, 0);
// show text to the right of the icon
canvas.drawText("GROUP", ptScreenCoord.x, ptScreenCoord.y + 30, paint);
}
private void drawSingle(Canvas canvas, MapView mapView,
List<OverlayItem> markerList) {
for (OverlayItem item : markerList) {
GeoPoint point = item.getPoint();
Point ptScreenCoord = new Point();
mapView.getProjection().toPixels(point, ptScreenCoord);
Paint paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(30);
paint.setAntiAlias(true);
paint.setARGB(150, 0, 0, 0);
// show text to the right of the icon
canvas.drawText("SINGLE", ptScreenCoord.x, ptScreenCoord.y + 30,
paint);
}
}
public static boolean isWithin(Point p, MapView mapView) {
return (p.x > 0 & p.x < mapView.getWidth() & p.y > 0 & p.y < mapView
.getHeight());
}
}
Assuming your markers are grouped together in an ItemizedOverlay you could create a method which was called when the map was zoomed. This would compare pixel co-ordinates of each marker to see if they overlap and set a flag. Then in the draw method you could draw either the grouped marker or individuals;
Something like:
//this would need to be wired to be called when the mapview is zoomed
//it sets the drawgrouped flag if co-ordinates are close together
Boolean drawGrouped=false;
public void onMapZoom(MapView mapView){
//loop thru overlay items
Integer i,l=this.size();
OverlayItem item;
Integer deltaX=null,deltaY=null;
Projection proj = mapView.getProjection();
Point p=new Point();
Integer x=null,y=null;
Integer tolerance = 10; //if co-ordinates less than this draw grouped icon
for(i=0;i<l;i++){
//get the item
item=this.getItem(i);
//convert the overlays position to pixels
proj.toPixels(item.getPoint(), p);
proj.toPixels(item.getPoint(), p);
//compare co-ordinates
if(i==0){
x=p.x;
y=p.y;
continue;
}
deltaX=Math.abs(p.x-x);
deltaY=Math.abs(p.y-y);
//if the co-ordinates are too far apart dont draw grouped
if(deltaX>tolerance || deltaY>tolerance){
drawGrouped=false;
return;
}
x=p.x;
y=p.y;
}
//all co-ords are within the tolerance
drawGrouped=true;
}
public void draw(android.graphics.Canvas canvas, MapView mapView, boolean shadow){
if(drawGrouped==true){
//draw the grouped icon *needs to be optimised to only do it once
drawGrouped(canvas,mapView,shadow);
return;
}
//not grouped do regular drawing
super.draw(canvas, mapView, shadow);
}
What you are looking for is usually called clustering. There are common techniques to do this, you can refer, for example, to this SO question, it leads to this post.
The basic idea is to divide the map on squares based on the current zoom level (you can cache calculations based on the zoom level to avoid recalculation when the user starts zooming), and to group them based which square they belong to. So you end up having some sort of grouping based on zoom level, ie for level 1-5 just draw the markers, for level 5-8 group them in squares of 20 miles, for 9-10 in squares of 50 miles, and so on.
Here is another relevant question on SO that you may want to take a look, not sure about the performance of this though: Android Maps Point Clustering
If your markers are grouped, you'll have a fair idea at what zoom level you should be displaying individual markers or the group marker e.g. zoom level > 17 then display individual markers, otherwise display the group marker. I used code something like this in my ItemizedOverlay to change my markers:
#Override
public void draw(Canvas canvas, MapView mapv, boolean shadow)
{
int zoom = mapv.getZoomLevel();
switch(zoom)
{
case 19:
setMarkersForZoomLevel19();
break;
case 18:
setMarkersForZoomLevel18();
break;
case 17:
setMarkersForZoomLevel17();
break;
case 16:
setMarkersForZoomLevel16();
break;
default:
// Hide the markers or remove the overlay from the map view.
mapv.getOverlays().clear();
}
area.drawArea(canvas, mapv);
// Putting this call here rather than at the beginning, ensures that
// the Overlay items are drawn over the top of canvas stuff e.g. route lines.
super.draw(canvas, mapv, false);
}
private void setMarkersForZoomLevel19()
{
for (JourneyOverlayItem item : mOverlays)
{
item.setMarker(areaPointIcon48);
}
}
If its possible to have the individual markers in a collection, you could easily get the largest and smallest latitude and longitude and the difference between them will give you the latitude and longitude span (this could then be used to zoom to the span to show the group of markers). Divide the spans by 2 and you should have the centre point for placing the group marker.
This is the approach that I used. However, it's O(n^2).
The pins must be sorted based on prominent.
Pick pin with the highest prominent. Look at all pins around it. Absorb pins near that pin.
Then move on to the next highest prominent pin. Do the same. Repeat.
Simple.
Things get complicated if you move the map around, zooming in, zooming out, and you want to ensure that the new pins are not being redrawn. So you check each cluster if they have to split during zoom in, and then you check each cluster if they have to merge during zoom out. Then you remove pins that's gone and add new pins. For every pin you add, you check whether they should join a cluster or form their own cluster.
This is the code I'm using to draw route. When i have 1000 of points, route severely slows ui. Maybe someone could provide a code snippet or a link which explains how to do route drawing more efficiently? I know that one way to solve this is caching path to bitmap, but have no idea how to do it.
public class PathOverlay extends Overlay{
private GeoPoint startPoint;
private GeoPoint finishPoint;
private ArrayList<GeoPoint> pathPoints;
private Paint paint;
private Path path;
private Point pathStartPoint;
private Point pathEndPoint;
private float dx;
private float dy;
public PathOverlay(GeoPoint startPoint, GeoPoint finishPoint, ArrayList<GeoPoint> pathPoints, int color){
this.startPoint = startPoint;
this.finishPoint = finishPoint;
this.pathPoints = pathPoints;
this.paint = new Paint();
this.paint.setAntiAlias(true);
this.paint.setDither(true);
this.paint.setColor(color);
this.paint.setAlpha(150);
this.paint.setStrokeWidth(4);
this.paint.setStyle(Paint.Style.STROKE);
}
#Override
public void draw(Canvas overlayCanvas, MapView mapView, boolean shadow) {
if(path == null) {
path = getPath(mapView);
} else {
path = transformPath(mapView);
}
overlayCanvas.drawPath(path, paint);
super.draw(overlayCanvas, mapView, shadow);
}
private Path getPath(MapView mapView) {
Projection projection = mapView.getProjection();
if(path == null) {
path = new Path();
path.setFillType(FillType.WINDING);
} else {
path.rewind();
}
Point point = new Point();
pathStartPoint = new Point();
pathEndPoint = new Point();
projection.toPixels(startPoint, point);
projection.toPixels(startPoint, pathStartPoint);
path.moveTo(point.x, point.y);
path.addCircle(point.x, point.y, (float) 2.0, Direction.CCW);
if (pathPoints != null) {
for(int i=0;i<pathPoints.size();i++) {
projection.toPixels(pathPoints.get(i), point);
path.lineTo(point.x, point.y);
}
}
projection.toPixels(finishPoint, point);
projection.toPixels(finishPoint, pathEndPoint);
path.lineTo(point.x-5, point.y);
path.addCircle(point.x-5, point.y, (float) 2.0, Direction.CCW);
return path;
}
private Path transformPath(MapView mapView) {
Projection projection = mapView.getProjection();
Point sPoint = new Point();
Point ePoint = new Point();
projection.toPixels(startPoint, sPoint);
projection.toPixels(finishPoint, ePoint);
float sx = ((float)ePoint.x - (float)sPoint.x)/((float)pathEndPoint.x - (float)pathStartPoint.x);
float sy = ((float)ePoint.y - (float)sPoint.y)/((float)pathEndPoint.y - (float)pathStartPoint.y);
if(sx != 1.0 && sy != 1.0) {
Log.i("PathOverlay", "resized");
return getPath(mapView);
} else {
Log.i("PathOverlay", "moved");
Matrix matrix = new Matrix();
dx = (float)sPoint.x - (float)pathStartPoint.x;
dy = (float)sPoint.y - (float)pathStartPoint.y;
matrix.postTranslate(dx, dy);
pathStartPoint = sPoint;
pathEndPoint = ePoint;
path.transform(matrix);
return path;
}
}
}
You can draw the path to a transparent Bitmap object (whatever size you see fitting - the bigger it is, the better the detail of the path yet higher memory consumption).
Make sure you create it with Bitmap.config.ARGB_8888 for transparency.
Once you've done this, you'll be using two rectangles to display the path on the Overlay:
A source rectangle to determine which part of the path is visible
A destination rectangle to determine where you want to display this piece of the path on the Overlay's canvas.
You'll be using Canvas.drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
Shouldn't be too difficult, you've done most of the important calculations in your transformPath method.
Added:
You can actually do a combination of both holding a path drawn to a Bitmap and redrawing the actual path points. Use the technique described above for when the user moves around the map or zooms in/out then redraw the path when the user lets go of the screen.
Best way to increase speed of drawing is reducing number of point the path includes. Probably they are not necessary - lot of them just lays between previous and next, so you can filter them by:
minimal distance from previous point (easy way)
minimal bearing change (a bit harder, although Location class and it's method bearingTo() should help.
You should know that draw() method draws one draw cycle about 50 times. I do not know why, but you can test it. This slows down the performance. And if you have to draw 1000 object and draw() draws them 30-50 each...it is getting very very clumsy..
To avoid this, you should create a cache overlay.This cache will draw all objects only once and will reject the other draws.
To speed up the process, draw in background thread with low priority.
The fastest way to draw a line between multiple points is to use the method Canvas.drawLines(float[] pts, int offset, int count, Paint paint).
I have tested all methods and this is the fastest method that android offers.