Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TileProvider graphics gets skewed on higher zoom levels

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:

graphics is skewed

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):

enter image description here

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);
    }
}
like image 687
Pavel Dudka Avatar asked Aug 11 '14 20:08

Pavel Dudka


Video Answer


1 Answers

I see you are using google's tileview, you can try and consider mogarius's library which is an
open source on github

I never tried it before but it support most of the functionalities you need (markers\dots
and dynamic path drawing) out of the box so that'll save you the time spending on making
matrix calculations for upscaling and downscaling.
there is also a demo video for some of the usage he made, and a great javadoc he published.

like image 136
crazyPixel Avatar answered Nov 07 '22 08:11

crazyPixel