Im currently playing with TileProver
in Android Maps API v2 and kinda stuck with the following problem: graphics which I paint manually into Bitmap gets skewed significantly on higher zoom levels:
Let me explain what Im doing here. I have number of LatLng points and I draw a circle for every point on a map, so as you zoom in - point stays at the same geo location. As you can see on the screenshot, circles look fine on lower zoom levels, but as you start zooming in - circles get skewed..
That's how it is implemented:
package trickyandroid.com.locationtracking;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.Log;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import java.io.ByteArrayOutputStream;
/**
* Created by paveld on 8/8/14.
*/
public class CustomTileProvider implements TileProvider {
private final int TILE_SIZE = 256;
private int density = 1;
private int tileSizeScaled;
private Paint circlePaint;
private SphericalMercatorProjection projection;
private Point[] points;
public CustomTileProvider(Context context) {
density = 3; //hardcoded for now, but should be driven by DisplayMetrics.density
tileSizeScaled = TILE_SIZE * density;
projection = new SphericalMercatorProjection(TILE_SIZE);
points = generatePoints();
circlePaint = new Paint();
circlePaint.setAntiAlias(true);
circlePaint.setColor(0xFF000000);
circlePaint.setStyle(Paint.Style.FILL);
}
private Point[] generatePoints() {
Point[] points = new Point[6];
points[0] = projection.toPoint(new LatLng(47.603861, -122.333393));
points[1] = projection.toPoint(new LatLng(47.600389, -122.326741));
points[2] = projection.toPoint(new LatLng(47.598942, -122.318973));
points[3] = projection.toPoint(new LatLng(47.599000, -122.311549));
points[4] = projection.toPoint(new LatLng(47.601373, -122.301721));
points[5] = projection.toPoint(new LatLng(47.609764, -122.311850));
return points;
}
@Override
public Tile getTile(int x, int y, int zoom) {
Bitmap bitmap = Bitmap.createBitmap(tileSizeScaled, tileSizeScaled, Bitmap.Config.ARGB_8888);
float scale = (float) (Math.pow(2, zoom) * density);
Matrix m = new Matrix();
m.setScale(scale, scale);
m.postTranslate(-x * tileSizeScaled, -y * tileSizeScaled);
Canvas c = new Canvas(bitmap);
c.setMatrix(m);
for (Point p : points) {
c.drawCircle((float) p.x, (float) p.y, 20 / scale, circlePaint);
}
return bitmapToTile(bitmap);
}
private Tile bitmapToTile(Bitmap bmp) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] bitmapdata = stream.toByteArray();
return new Tile(tileSizeScaled, tileSizeScaled, bitmapdata);
}
}
Logic tells me that this is happening because I'm translating LatLng into screen position only for 1 tile (256x256 which is zoom level 0) and then in order to translate this screen point to other zoom levels, I need to scale my bitmap and translate it to appropriate position. At the same time, since bitmap is scaled, I need to compensate circle radius, so I divide radius by scale factor. So at zoom level 19 my scale factor is already 1572864 which is huge. It is like looking at this circle via huge magnifying glass. That's why I have this effect.
So I suppose the solution would be to avoid bitmap scaling and scale/translate only screen coordinates. In this case my circle radius will be always the same and will not be downscaled.
Unfortunately, matrix math is not my strongest skill, so my question is - how do I scale/translate set of points for arbitrary zoom level having set of points calculated for zoom level '0'?
The easiest way for doing this would be to have different Projection instances for each zoom level, but since GeoPoint -> ScreenPoint translation is quite expensive operation, I would keep this approach as a back-up and use some simple math for translating already existing screen points.
NOTE
Please note that I need specifically custom TileProvider
since in the app I will be drawing much more complicated tiles than just circles. So simple Marker
class is not going to work for me here
UPDATE Even though I figured out how to translate individual points and avoid bitmap scaling:
c.drawCircle((float) p.x * scale - (x * tileSizeScaled), (float) p.y * scale - (y * tileSizeScaled), 20, circlePaint);
I still don't know how to do this with Path
objects. I cannot translate/scale path like you would do this with individual points, so I still have to scale my bitmap which causes drawing artifacts again (stroke width is skewed on higher zoom levels):
And here is a code snippet:
package trickyandroid.com.locationtracking;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import java.io.ByteArrayOutputStream;
/**
* Created by paveld on 8/8/14.
*/
public class CustomTileProvider implements TileProvider {
private final int TILE_SIZE = 256;
private int density = 1;
private int tileSizeScaled;
private SphericalMercatorProjection projection;
private Point[] points;
private Path path;
private Paint pathPaint;
public CustomTileProvider(Context context) {
density = 3; //hardcoded for now, but should be driven by DisplayMetrics.density
tileSizeScaled = TILE_SIZE * density;
projection = new SphericalMercatorProjection(TILE_SIZE);
points = generatePoints();
path = generatePath(points);
pathPaint = new Paint();
pathPaint.setAntiAlias(true);
pathPaint.setColor(0xFF000000);
pathPaint.setStyle(Paint.Style.STROKE);
pathPaint.setStrokeCap(Paint.Cap.ROUND);
pathPaint.setStrokeJoin(Paint.Join.ROUND);
}
private Path generatePath(Point[] points) {
Path path = new Path();
path.moveTo((float) points[0].x, (float) points[0].y);
for (int i = 1; i < points.length; i++) {
path.lineTo((float) points[i].x, (float) points[i].y);
}
return path;
}
private Point[] generatePoints() {
Point[] points = new Point[10];
points[0] = projection.toPoint(new LatLng(47.603861, -122.333393));
points[1] = projection.toPoint(new LatLng(47.600389, -122.326741));
points[2] = projection.toPoint(new LatLng(47.598942, -122.318973));
points[3] = projection.toPoint(new LatLng(47.599000, -122.311549));
points[4] = projection.toPoint(new LatLng(47.601373, -122.301721));
points[5] = projection.toPoint(new LatLng(47.609764, -122.311850));
points[6] = projection.toPoint(new LatLng(47.599221, -122.311531));
points[7] = projection.toPoint(new LatLng(47.599663, -122.312410));
points[8] = projection.toPoint(new LatLng(47.598823, -122.312614));
points[9] = projection.toPoint(new LatLng(47.599959, -122.310651));
return points;
}
@Override
public Tile getTile(int x, int y, int zoom) {
Bitmap bitmap = Bitmap.createBitmap(tileSizeScaled, tileSizeScaled, Bitmap.Config.ARGB_8888);
float scale = (float) (Math.pow(2, zoom) * density);
Canvas c = new Canvas(bitmap);
Matrix m = new Matrix();
m.setScale(scale, scale);
m.postTranslate(-x * tileSizeScaled, -y * tileSizeScaled);
c.setMatrix(m);
pathPaint.setStrokeWidth(6 * density / scale);
c.drawPath(path, pathPaint);
return bitmapToTile(bitmap);
}
private Tile bitmapToTile(Bitmap bmp) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] bitmapdata = stream.toByteArray();
return new Tile(tileSizeScaled, tileSizeScaled, bitmapdata);
}
}