How to draw a grid over google maps in android?

半城伤御伤魂 提交于 2020-01-02 09:57:12

问题


I want to create a grid like What3Words app did.

Once the camera zooms past a certain level the grid shows up and scales in size as the user zooms.

I've tried TileOverlay and successfully created a grid. The problem was that the grid redraws itself with every zoom. I want the grid rather than redrawn, to be scaled with the zoom level.

I've then moved on to GroundOverlay and used an already drawn grid and I found two issues: The image quality is worse than the original and, like the TileOverlay, it doesn't scale with the zooming.

Then I tried to use polygons to draw the squares and use the longitude and latitude provided by (Map.getProjection().getVisibleRegion()) and because the earth is a sphere the grid's size is inconsistent on different areas.

And now I'm using a canvas to draw it manually.

Does any of you have any idea how to achieve what I'm trying to do?

Thanks in advance.


回答1:


Ok, this answer demonstrates the graphic update to draw and move the grid and also an attempt to align the grid using the w3w API.

So one issue with using the w3w seems to be how to compute the location of a grid cell. Since the algorithm evidently is private, for this implementation the 'grid' rest api call is used for the current screen center point (on idle) and the json response parsed for a candidate reference point.

In this example a polygon is always drawn for the "reference" grid cell obtained from the w3w grid call.

The grid view implementation uses the canvas.translate call to properly align and draw the grid using an offset computed from the reference point.

This works at any latitude due to the use of the SphericalUtil usage for mapping distance to screen pixels.

Recording at bottom (low quality).

Main Activity

Here the w3w grid rest call is made on camera idle and zoom far enough out (there's no need to keep realigning in close zoom) and the result (a corner point of a nearby grid cell to be used as a reference point) is fed to the grid view. A filled polygon is drawn to denote the reference grid cell.

On camera movement, the same reference point is used but the current screen position is used to maintain the proper offsets.

public void what3words() {

    // some initial default position
    LatLng pt = new LatLng(38.547279, -121.461019);

    mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);

    // move map to a coordinate
    CameraUpdate cu = CameraUpdateFactory.newLatLng(pt);
    mMap.moveCamera(cu);
    cu = CameraUpdateFactory.zoomTo(14);
    mMap.moveCamera(cu);

    mMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() {
        @Override
        public void onMapClick(LatLng latLng) {
            mMap.addCircle(new CircleOptions().radius(4).strokeColor(Color.BLUE).center(latLng));
        }
    });

    // while the camera is moving just move the grid (realign on idle)
    mMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
        @Override
        public void onCameraMove() {
            ((GridView) findViewById(R.id.grid_any)).setAlignment(
                    null, mMap.getProjection(), mMap.getProjection().getVisibleRegion());
        }
    });

    // on idle fetch the grid using the screen center point
    mMap.setOnCameraIdleListener(new GoogleMap.OnCameraIdleListener() {
        @Override
        public void onCameraIdle() {
            Log.d(TAG,"idle");


            final LatLng centerOfGridCell = mMap.getCameraPosition().target;

            if (!gridSet || mMap.getCameraPosition().zoom < 10) {
                getGrid(centerOfGridCell, new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        Log.d(TAG, "reqpt: " + centerOfGridCell + " volley response: " + response);
                        try {
                            JSONObject jsonObject = new JSONObject(response);
                            JSONArray jsonArray = jsonObject.getJSONArray("lines");
                            JSONObject firstList = jsonArray.getJSONObject(1);
                            JSONObject firstPt = firstList.getJSONObject("start");
                            String lat = firstPt.getString("lat");
                            String lng = firstPt.getString("lng");
                            Log.d(TAG, "lat: " + lat + " lng: " + lng);

                            LatLng alignmentPt = new LatLng(Double.parseDouble(lat), Double.parseDouble(lng));
                            Projection p = mMap.getProjection();
                            VisibleRegion vr = p.getVisibleRegion();


                            ((GridView) findViewById(R.id.grid_any)).setAlignment(alignmentPt, p, vr);

                            if (polygon != null) {
                                polygon.remove();
                            }

                            // take alignment point and draw 3 meter square polygon
                            LatLng pt1 = SphericalUtil.computeOffset(alignmentPt, 3, 90);
                            LatLng pt2 = SphericalUtil.computeOffset(pt1, 3, 180);
                            LatLng pt3 = SphericalUtil.computeOffset(pt2, 3, 270);

                            polygon = mMap.addPolygon(new PolygonOptions().add(alignmentPt,
                                    pt1, pt2, pt3, alignmentPt)
                                    .strokeColor(Color.BLUE).strokeWidth(4).fillColor(Color.BLUE));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

                });

                gridSet = true;
            }


        }
    });
}


// Issue request to w3w - REMEMBER TO REPLACE **YOURKEY** ...
private void getGrid(LatLng pt, Response.Listener<String> listener) {

    // something 9 meters to east
    LatLng otherPt = SphericalUtil.computeOffset(pt, 6.0, 225);
    String bboxStr = Double.toString(pt.latitude)+","+
            Double.toString(pt.longitude)+","+
            Double.toString(otherPt.latitude)+","+
            Double.toString(otherPt.longitude);
    RequestQueue rq = Volley.newRequestQueue(this);
    String url = "https://api.what3words.com/v2/grid?bbox="+bboxStr+"&format=json&key=YOURKEY";

    Log.d(TAG,"url="+url);
    StringRequest req = new StringRequest(Request.Method.GET, url, listener, new Response.ErrorListener() {

        @Override
        public void onErrorResponse(VolleyError error) {
            Log.e(TAG, "volley error: "+error);
        }
    });

    rq.add(req);
}

Grid View

The grid view extends View and is in the map layout as a sibling to the map fragment.

The interestings parts are:

setAlignment - here the screen pixel extent of 3 meters is computed using the SphericalUtil class. This screen pixel dimension representing a 3 meter extent (at the provided reference location) is then used to align the grid by computing x/y offsets (which are then used in the onDraw). Note this auto-scales the grid using the 'SphericalUtil.computeOffset' utility to measure a point 3 meters east and as a result compute the screen pixel equivalent of 3 meters.

onDraw - here the translate method of canvas is used to repeat the grid shape starting at the computed offset (in setAlignment).

public class GridView extends View {

    private static final String TAG = GridView.class.getSimpleName();

    BitmapDrawable bm;
    Bitmap bitmap;

    GradientDrawable gd;
    int offsetX = 0;
    int offsetY = 0;
    private int width = 16;


    public GridView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    public void setWidth(int w) {
        width = w;
        render();
        invalidate();
    }


    private void render() {
        setShape();
        if (gd != null) {
            bitmap = drawableToBitmap(gd);
            bm = new BitmapDrawable(getResources(), bitmap);
            bm.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
            bm.setBounds(0, 0, width, width);
        }
    }

    Point startPt;
    LatLng savedAlignmentPt;


    public void setAlignment(LatLng alignmentPt, Projection p, VisibleRegion vr) {

        if (alignmentPt == null) {
            alignmentPt = savedAlignmentPt;
        }

        if (alignmentPt == null) {
            return;
        }
        // the alignment point is the a corner of a grid square "near" the center of the screen
        savedAlignmentPt = alignmentPt;

        // compute how many screen pixels are in 3 meters using alignment point
        startPt =  p.toScreenLocation(alignmentPt);
        LatLng upperRight = SphericalUtil.computeOffset(alignmentPt, 3, 90);
        Point upperRtPt = p.toScreenLocation(upperRight);

        int pixelsOf3meters = upperRtPt.x - startPt.x;

        // don't draw grid if too small
        if (pixelsOf3meters < 16) {
            return;
        }

        setWidth(pixelsOf3meters);

        // startPt is screen location of alignment point
        offsetX = (pixelsOf3meters - (startPt.x % pixelsOf3meters));
        offsetY = (pixelsOf3meters - (startPt.y % pixelsOf3meters));

        invalidate();

    }

    private void setShape() {
        int left, right, top, bottom;
        gd = new GradientDrawable();
        gd.setShape(GradientDrawable.RECTANGLE);
        gd.setSize(width, width);
        gd.setStroke(2, Color.argb(0xff, 0xcc, 0x22, 0x22));

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        Rect rect = canvas.getClipBounds();


        final int cWidth = canvas.getWidth();
        final int cHeight = canvas.getHeight();

        if (bm == null) {
            return;
        }

        final Rect bmRect = bm.getBounds();
        if (bmRect.width() <= 8 || bmRect.height() <= 8) {
            return;
        }


        final int iterX = iterations(cWidth, -offsetX, bmRect.width());
        final int iterY = iterations(cHeight, -offsetY, bmRect.height());

        canvas.translate(-offsetX, -offsetY);

        for (int x = 0; x < iterX; x++) {
            for (int y = 0; y < iterY; y++) {
                bm.draw(canvas);
                canvas.translate(.0F, bmRect.height());
            }
            canvas.translate(bmRect.width(), -bmRect.height() * iterY);
        }
    }

    private static int iterations(int total, int start, int side) {
        final int diff = total - start;
        final int base = diff / side;
        return base + (diff % side > 0 ? 1 : 0);
    }

    public static Bitmap drawableToBitmap (Drawable drawable) {
        Bitmap bitmap = null;

        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            if(bitmapDrawable.getBitmap() != null) {
                return bitmapDrawable.getBitmap();
            }
        }

        if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
        } else {
            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        }

        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }
}

Notes:

  • The translate stuff was referenced from: https://github.com/noties/ScrollingBackgroundView/blob/master/library/src/main/java/ru/noties/sbv/ScrollingBackgroundView.java.
  • The w3w grid api result is not well documented so I just picked a point in the returned list as a candidate cell corner point.
  • The 'getGrid' method simply uses volley to issue the w3w request and most of the main activity code is in the response listener: https://developer.android.com/training/volley/


I'm having my own battle with the w3w grid API. When I compute the distance between start/end points for each point in the list returned, I get 4.24264 meters so clearly I'm not getting something. Here's a simple method to show results and the screen shot (white=current center used in request, any other color=start-end of a point pair in list; end points have black outline). Here also it becomes clear which point is used to align the grid.

Interestingly though, the start of one "line" does appear to be 3 meters from the start of the next line (compare red-start to blue-start):

Code:

 // plot each point as a circle
 for (int i = 0; i < jsonArray.length(); i++) {
     JSONObject startPt = jsonArray.getJSONObject(i).getJSONObject("start");
     JSONObject endPt = jsonArray.getJSONObject(i).getJSONObject("end");
     LatLng start = new LatLng(Double.parseDouble(startPt.getString("lat")), Double.parseDouble(startPt.getString("lng")));
     LatLng end = new LatLng(Double.parseDouble(endPt.getString("lat")), Double.parseDouble(endPt.getString("lng")));
     int c = colors[(i % colors.length)];
     mMap.addCircle(new CircleOptions().center(start).strokeColor(c).fillColor(c).radius(1));
     mMap.addCircle(new CircleOptions().center(end).strokeColor(Color.BLACK).fillColor(c).radius(1).strokeWidth(2));

     Log.d(TAG, "d  = "+SphericalUtil.computeDistanceBetween(start,end));
 }
 mMap.addCircle(new CircleOptions().center(centerOfGridCell).strokeColor(Color.WHITE).radius(1).strokeWidth(4));



回答2:


Here's the stationary approach which you may already have tried. In summary, create Shapes for each grid size (in this example small, medium, large); create views in main layout, one for each grid size (all visibility 'Gone' or one visible); create a class to extend View just so the shape can be tiled (I did not know how to set shape tiled in xml); and in this test the zoom listener changes the "Visible" views on zoom < 10 (small) ; zoom = 11 (medium); zoom > 11 (large).

First the recording and then the code:

Code

Shapes

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" 
android:shape="rectangle" >
    <size android:width="8dp" android:height="8dp" />
    <stroke android:width="1px" android:color="@color/gridLine" />
</shape>

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"  >
    <size android:width="12dp" android:height="12dp" />
    <stroke android:width="1px" android:color="@color/gridLine" />
</shape>

<shape xmlns:android="http://schemas.android.com/apk/res/android"  android:shape="rectangle" >
    <size android:width="16dp" android:height="16dp" />
    <stroke android:width="1px" android:color="@color/gridLine" />
</shape>

Main Layout (the grid views (only at most one of which is ever visible) are peers to the map and are overtop)

<RelativeLayout android:layout_height="match_parent" android:layout_width="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android">

<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:map="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="stuff.MapsActivity" />

<stuff.GridView
    android:id="@+id/grid_small"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/grid_square_small"
    android:visibility="visible"/>

    <stuff.GridView
        android:id="@+id/grid_medium"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/grid_square_medium"
        android:visibility="gone"/>

    <stuff.GridView
        android:id="@+id/grid_large"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/grid_square_large"
        android:visibility="gone"/>

</RelativeLayout>

GridView (nothing exciting here; just used to set shape as REPEAT which I did not know how to do in xml).

package stuff;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;

public class GridView extends View {
    BitmapDrawable bm;

    public GridView(Context context, AttributeSet attrs) {
        super(context, attrs);

        Drawable d= getBackground();
        if (d != null) {
            Bitmap b = drawableToBitmap(d);
            bm = new BitmapDrawable(getResources(), b);
            bm.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
        }

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        bm.setBounds(canvas.getClipBounds());
        bm.draw(canvas);
    }

    public static Bitmap drawableToBitmap (Drawable drawable) {
        Bitmap bitmap = null;

        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            if(bitmapDrawable.getBitmap() != null) {
                return bitmapDrawable.getBitmap();
            }
        }

        if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
        } else {
            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        }

        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }
}

Main Activity (Map) (just the camera listener)

int lastZoom = -1;
public void gridTest() {
    mMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
        @Override
        public void onCameraMove() {
            int zoomInt = (int) mMap.getCameraPosition().zoom;
            if (lastZoom == -1) {
                lastZoom = zoomInt;
                return;
            }
            Log.d(TAG, "zoom= "+zoomInt);
            if (zoomInt < 10) {
                findViewById(R.id.grid_small).setVisibility(View.VISIBLE);
                findViewById(R.id.grid_medium).setVisibility(View.GONE);
                findViewById(R.id.grid_large).setVisibility(View.GONE);
            } else if (zoomInt < 11) {
                findViewById(R.id.grid_small).setVisibility(View.GONE);
                findViewById(R.id.grid_medium).setVisibility(View.VISIBLE);
                findViewById(R.id.grid_large).setVisibility(View.GONE);
            } else {
                findViewById(R.id.grid_small).setVisibility(View.GONE);
                findViewById(R.id.grid_medium).setVisibility(View.GONE);
                findViewById(R.id.grid_large).setVisibility(View.VISIBLE);
            }
            lastZoom = zoomInt;
        }
    });
}

Notes:

  • The drawableToBitmap is from here: How to convert a Drawable to a Bitmap?
  • Notice the zoom control is occluded so that's not nice.
  • Maybe this could be modified to support panning as well - in the onDraw method of view to offset the grid in the x/y.
  • In theory, you could use the setBounds in the GridView.onDraw to support panning by adjusting 'left', 'top' based on panning movement. The range of adjustment would be [0, gridwidth) (since it's a square and repeating).


来源:https://stackoverflow.com/questions/52353180/how-to-draw-a-grid-over-google-maps-in-android

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!