java - How to draw a grid over google maps in android?
问题描述
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.
解决方案
好的,这个答案演示了绘制和移动网格的图形更新,以及使用 w3w API 对齐网格的尝试。
所以使用 w3w 的一个问题似乎是如何计算网格单元的位置。由于该算法显然是私有的,因此对于此实现,“网格”rest api 调用用于当前屏幕中心点(空闲时),并为候选参考点解析 json 响应。
在此示例中,始终为从 w3w 网格调用获得的“参考”网格单元绘制一个多边形。
网格视图实现使用 canvas.translate 调用来正确对齐并使用从参考点计算的偏移量绘制网格。
由于使用 SphericalUtil 将距离映射到屏幕像素,这在任何纬度都有效。
在底部录制(低质量)。
主要活动
这里 w3w 网格休息调用是在相机空闲时进行的,并且放大到足够远(无需在近距离缩放中保持重新对齐),结果(附近网格单元的角点用作参考点)被馈送到网格视图。绘制一个填充的多边形来表示参考网格单元。
在相机移动时,使用相同的参考点,但当前屏幕位置用于保持适当的偏移。
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);
}
网格视图
网格视图扩展了 View 并且在地图布局中作为地图片段的兄弟。
有趣的部分是:
setAlignment - 这里使用 SphericalUtil 类计算 3 米的屏幕像素范围。该屏幕像素尺寸表示 3 米范围(在提供的参考位置),然后用于通过计算 x/y 偏移(然后在 onDraw 中使用)来对齐网格。请注意,这会使用“SphericalUtil.computeOffset”实用程序自动缩放网格,以测量东边 3 米的点,从而计算出相当于 3 米的屏幕像素。
onDraw - 这里 canvas 的 translate 方法用于从计算的偏移量开始重复网格形状(在 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;
}
}
笔记:
- 翻译内容引用自:https ://github.com/noties/ScrollingBackgroundView/blob/master/library/src/main/java/ru/noties/sbv/ScrollingBackgroundView.java 。
- w3w 网格 api 结果没有得到很好的记录,所以我只是在返回的列表中选择了一个点作为候选单元格角点。
- 'getGrid' 方法只是使用 volley 发出 w3w 请求,大部分主要活动代码都在响应侦听器中:https ://developer.android.com/training/volley/
我正在与 w3w 网格 API 进行自己的战斗。当我计算返回列表中每个点的起点/终点之间的距离时,我得到 4.24264 米,很明显我没有得到任何东西。这是显示结果和屏幕截图的简单方法(白色=请求中使用的当前中心,任何其他颜色=列表中点对的开始-结束;端点有黑色轮廓)。这里也很清楚哪个点用于对齐网格。
有趣的是,一条“线”的起点似乎距离下一条线的起点 3 米(比较红色起点和蓝色起点):
代码:
// 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));
推荐阅读
- gulp - Gulp src 找不到模式
- sql - Oracle - 根据字符串的总长度划分字符串
- javascript - 如何使用 React 在客户端裁剪图像而不会丢失分辨率
- javascript - React:useState 向我显示了以前的状态值,而当我查看 DevTools 时它会发生变化
- ios - Xcode Build failed,fatal error: module 'firebase_auth' not found @import firebase_auth;
- netsuite - SuiteScript - 在采购订单行上创建弹出链接
- sql - 如何返回没有空值的记录
- python - 尝试通过 ajax(fetch) 在 django 中保存数据时出错
- microsoft-teams - Microsoft 团队不允许我更改我的信息
- sql - MS Access 和 SQL Server 加密