I'm working on a simple Android app for plotting routes on a map. All is going well, but I have an issue when zooming in on my Samsung Galaxy S2. It works fine on a Galaxy S3, so I'm wondering whether it's related to memory management on the lower specced device. It also works fine on the emulator.
Here is equivalent code located in the overlays onDraw method, just condensed for posting here:
Point current = new Point();
Path path = new Path();
Projection projection = mapView.getProjection();
Iterator<GeoPoint> iterator = pointList.iterator();
if (iterator.hasNext()) {
projection.toPixels(iterator.next(), current);
path.moveTo((float) current.x, (float) current.y);
} else return path;
while(iterator.hasNext()) {
projection.toPixels(iterator.next(), current);
path.lineTo((float) current.x, (float) current.y);
}
Paint roadPaint = new Paint();
roadPaint.setAntiAlias(true);
roadPaint.setStrokeWidth(8.0f);
roadPaint.setColor(Color.BLACK);
roadPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, roadPaint);
It's not too dissimilar to most of the sample code floating around for doing this. I'm just wondering if anyone can confirm my suspicions and advise if there is anything I can do in terms of configuration or tweaks that I can do to force drawing at all zoom levels?
Thanks in advance.
Cheers,
Nathan
The problem is that you are painting the overlay yourself for a very specific state of the mapview. You should use OverlayItem instead.
The OverlayItem is added to the MapView overlays collection, and the MapView handles all the re-drawing depending on it's own state ( zoom, location, etc )
#Override
public void draw( Canvas canvas, MapView mapView, boolean shadow )
{
super.draw( canvas, mapView, shadow );
int x1 = -1;
int y1 = -1;
int x2 = -1;
int y2 = -1;
Paint paint = new Paint();
paint.setStyle( Paint.Style.STROKE );
paint.setColor( GeoLocation.ROUTE_COLOR );
paint.setStrokeWidth( STROKE_WIDTH );
for ( int i = 0; i < mRouteGeoPoints.size(); i++ )
{
Point point = new Point();
mapView.getProjection().toPixels( geoPoints.get( i ), point );
x2 = point.x;
y2 = point.y;
if ( i > 0 )
{
canvas.drawLine( x1, y1, x2, y2, paint );
}
x1 = x2;
y1 = y2;
}
}
You said that code above was an equivalent (not the real code you are running) and that's clear because you are returning a Path object in a onDraw() which you couldn't.
The "compressed form" of code you show should work as well as using the drawLine(). So the problem should come from something else (may the original code).
Anyway, I'll give you a couple of hints:
When the top and bottom of object you are drawing to a canvas are both out of screen, the object is ignored and not drawn. Check if this is not whats happening with your path. See my answer in this post Android Map Overlay Disappears on Zoom
You don't need to rebuild the path object every time. You are probably already doing it, and that's why you made the short version above. See my answer in this post with some suggestions to improve path drawing: Overlay behavior when zooming
If for some reason you really want to use the slower approach of drawLine(), you can use the follwing to make the line look better:
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setColor(...);
paint.setAlpha(...);
paint.setStrokeWidth(...);
Finally, if the issue remains, update your question with more relevant code and let me know. Maybe I can help further.
Regards.
Related
Ok, I am developing a sidescrolling game and my problem is on how to properly draw and update the screen. I am drawing on a SurfaceView and I use Path to make the contourns, currently the algorithm only draws this:
And I am sidescrolling by using Path.offSet() and then canvas.drawPath(), later on I update the last X position on the path by using Path.addRect() (and thats basically how I am drawing everything: using Path.addRect())
So here is the thread that updates the screen:
#Override
public void run() {
int x = LibraryLoader.getTerrainSizeX();
int y = LibraryLoader.getTerrainSizeY();
int count = 0;
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
Path path = new Path();
makePath(path, x, y, 0, LibraryLoader.getTerrainThickness());
Path path2 = new Path();
makePath(path2, x, y, LibraryLoader.getTerrainThickness(), y);
while (run) {
Canvas c = null;
try {
c = surfaceHolder.lockCanvas(null);
synchronized (surfaceHolder) {
fps = fps();
drawMyData(c, path, path2, paint, fps);
LibraryLoader.updateOffSet();
updatePaths(path, path2, x, y);
if ((count++) == (x / 2) - 1) {
LibraryLoader.updateOffSetArray();
count = 0;
}
}
} finally {
if (c != null) {surfaceHolder.unlockCanvasAndPost(c);}
}
}
and its respective methods:
public void updatePaths(Path path, Path path2, int x, int y) {
path.offset(-1f, 0);
path.addRect(x-3, topValue, x-2, bottomValue, Path.Direction.CW);
path2.offset(-1f, 0);
path2.addRect(x-3, topValue, x-2, y, Path.Direction.CW);
}
So, in my phone it works perfectly at 60fps, the problem is I tested in a lower end device and it begins at 40fps then drops every update until it gets below 10fps...(and keeps dropping). I guess I need to clean the state of the path, or I shouldn't even be using the Path class to begin with. So my question is how should I update the screen with the best performance? Obs: The canvas is not hardware accelerated.
Well folks I figured out that I was wrong about everything I did. The answer is simple: If your android application updates the whole screen every frame, use Opengl. Canvas is for app design for what I've seen, hope I am not mistaken. For example, if you want to make a custom animation for a LOGO or a button, so you use canvas, I guess. If anyone stumbles in this post do watch the videos Morrison Chang mentioned, they are very helpful to put you on the right track. Cheers.
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;
}
}
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
I've been trying to include the ability to show routes in an android app, and was working this solution into my app:
J2ME/Android/BlackBerry - driving directions, route between two locations
I've got basically all of the code in place, but in the drawPath method I get the error "The method toPixels(GeoPoint, Point) in the type Projection is not applicable for the arguments (GeoPoint, Point)" on the starred code below. Here's the code:
public void drawPath(MapView mMapView, Canvas canvas)
{
int x1 = -1, y1 = -1, x2 = -1, y2 = -1;
Paint paint = new Paint();
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
for (int i = 0; i < mPoints.size(); i++)
{
Point point = new Point();
mMapView.getProjection().*****toPixels*****(mPoints.get(i), point);
x2 = point.*****x*****;
y2 = point.*****y*****;
if (i > 0)
{
canvas.drawLine(x1, y1, x2, y2, paint);
}
x1 = x2;
y1 = y2;
}
}
I've not been able to test it at all yet because I've been unable to sort this error, so I don't know if there are other problems elsewhere. However in the meantime, if anybody knows why this error pops up it would be really appreciated. Thanks in advance! Oh and if anybody needs to see any of my other code or classes please let me know.
here is a good example i have posted you can try out this
I had tried this source code.
Copy the file from source code, don't change anything before that.
DON'T press Ctrl + Shift + O to load library class automatically.
Sometimes, eclipse import a wrong library. Why? That's another topic on eclipse.
Edit all these lines manually
import org.ci.geo.route.Road;
import org.ci.geo.route.RoadProvider;
Change import to your package library name.
Then edit this line that suit to your layout:
setContentView(R.layout.main);
mapView = (MapView) findViewById(R.id.mapview);
and this for sweet flavour
TextView textView = (TextView) findViewById(R.id.description);
Hope this will help you
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.