Для Google Maps Android SDK нет решения «из коробки» Polygon
заполнить шаблоном (только сплошная заливка), но вы можете использовать некоторые обходные пути.
TLDR;
Предположим, вам нужно заполнить многоугольник парка:
final List<LatLng> polygonPoints = new ArrayList<>();
polygonPoints.add(new LatLng(50.444477, 30.498976));
polygonPoints.add(new LatLng(50.445290, 30.500864));
polygonPoints.add(new LatLng(50.443958, 30.508921));
polygonPoints.add(new LatLng(50.443247, 30.508642));
polygonPoints.add(new LatLng(50.442830, 30.508878));
polygonPoints.add(new LatLng(50.440965, 30.508256));
polygonPoints.add(new LatLng(50.441949, 30.501443));
final Polygon polygon = mGoogleMap.addPolygon(new PolygonOptions()
.addAll(polygonPoints)
.strokeColor(Color.BLUE)
.fillColor(Color.argb(20, 0, 255, 0)));
с иконками деревьев (R.drawable.ic_forest
):
Вы можете сделать это через:
1) «заполнить» маркерами с помощью пользовательских (древовидных) значков. Для этого вам нужно найти прямоугольники границ полигонов и добавить маркеры в цикле в виде квадратной жадности. Для добавления маркеров только внутри полигона вы можете использовать PolyUtil.containsLocation()
из Библиотека утилит Google Maps Android API :
LatLngBounds bounds = getPolygonBounds(polygon.getPoints());
BitmapDescriptor icon = BitmapDescriptorFactory.fromResource(R.drawable.ic_forest);
double boundHeight = bounds.northeast.latitude - bounds.southwest.latitude;
double boundWidth = bounds.northeast.longitude - bounds.southwest.longitude;
double gridCntLat = 5; // 5 icons vertically
double gridCntLon = 5; // 5 icons horizontally
double dLat = (bounds.northeast.latitude - bounds.southwest.latitude) / gridCntLat;
double dLng = (bounds.northeast.longitude - bounds.southwest.longitude) / gridCntLon;
double lat = dLat + bounds.southwest.latitude;
while (lat < bounds.northeast.latitude) {
double lon = dLng + bounds.southwest.longitude;
while (lon < bounds.northeast.longitude) {
LatLng iconPos = new LatLng(lat, lon);
if (PolyUtil.containsLocation(iconPos, polygonPoints, true)) {
MarkerOptions markerOptions = new MarkerOptions().position(iconPos)
.draggable(false)
.flat(true)
.icon(icon);
mGoogleMap.addMarker(markerOptions);
}
lon += dLng;
}
lat += dLat;
}
, где getPolygonBounds()
:
private LatLngBounds getPolygonBounds(List<LatLng> polygon) {
LatLngBounds.Builder builder = new LatLngBounds.Builder();
for(int i = 0 ; i < polygon.size() ; i++) {
builder.include(polygon.get(i));
}
LatLngBounds bounds = builder.build();
return bounds;
}
и
...
частный GoogleMap mGoogleMap;
...
@Override
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;
...
}
В результате вы получили что-то вроде этого:
Это самое простое решение, но некоторые значки могут быть частично за пределами границы многоугольника (вы можете немного отрегулировать их с помощью метода MarkerOptions.anchor()
, а также можете установить MarkerOptions.flat(true)
, если хотите значки маркеров повернуты вместе с многоугольником) и при большом количестве маркеров карта начинает отставать.
2) использовать «динамический» (программно созданный для каждого многоугольника) GroundOverlay
. Для этого вам также необходимо определить границы многоугольника и затем создать наложение растрового изображения с такими же пропорциями в пикселях (также вам нужно ограничить его большей стороной, чтобы избежать ошибки нехватки памяти (OOM)). Обратите внимание, что вам нужно использовать Projection
для вычисления границ прямоугольника, потому что градусы широты на разных широтах и долготах имеют разный размер в метрах и в пикселях.
Тогда вам нужно нарисовать многоугольник на созданном растровом холсте. Для этого вы можете использовать Path
объект с пересчитанными точками многоугольника LatLng в локальные координаты растрового изображения в пикселях. Для этого вы должны использовать простую пропорцию, а не Projection.toScreenLocation()
:
screenPoint.x = (int) (width * (latLng.longitude - bounds.southwest.longitude) / boundWidth);
screenPoint.y = height - (int) (height * (latLng.latitude - bounds.southwest.latitude) / boundHeight); // y-axis of screen is inverted to Lat
Для заполнения полигона на растровом наложении растровыми изображениями ваших значков вы можете использовать Shader
и установить для него Paint
объект с Paint.setShader()
. Затем, наконец, вы можете добавить созданное растровое изображение как Ground Overlay на ваш объект GoogleMap. Полный исходный код метода fillPoly()
, который заполняет polygon
tileBitmap
:
private GroundOverlay fillPoly(Projection projection, Polygon polygon, Bitmap tileBitmap, float transparency) {
LatLngBounds bounds = getPolygonBounds(polygon.getPoints());
double boundHeight = bounds.northeast.latitude - bounds.southwest.latitude;
double boundWidth = bounds.northeast.longitude - bounds.southwest.longitude;
Point northEast = projection.toScreenLocation(bounds.northeast);
Point southWest = projection.toScreenLocation(bounds.southwest);
int screenBoundHeight = northEast.y - southWest.y;
int screenBoundWidth = northEast.x - southWest.x;
// determine overlay bitmap size
double k = Math.abs((double) screenBoundHeight / (double) screenBoundWidth);
int overlayWidth;
int overlayHeight;
if (Math.abs(boundWidth) > Math.abs(boundHeight)) {
overlayWidth = OVERLAY_MAX_SIZE;
overlayHeight = (int) (overlayWidth * k);
} else {
overlayHeight = OVERLAY_MAX_SIZE;
overlayWidth = (int) (overlayHeight * k);
}
// create overlay bitmap
Bitmap overlayBitmap = createOverlayBitmap(overlayWidth, overlayHeight, bounds, polygon.getPoints(), tileBitmap);
// create ground overlay
GroundOverlayOptions overlayOptions = new GroundOverlayOptions()
.image(BitmapDescriptorFactory.fromBitmap(overlayBitmap))
.transparency(transparency)
.positionFromBounds(bounds);
return mGoogleMap.addGroundOverlay(overlayOptions);
}
, где createOverlayBitmap()
:
private Bitmap createOverlayBitmap(int width, int height, LatLngBounds bounds, List<LatLng> polygon, Bitmap tileBitmap) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Path path = new Path();
BitmapShader shader = new BitmapShader(tileBitmap,
Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(10);
paint.setAntiAlias(true);
paint.setShader(shader);
List<Point> screenPoints = new ArrayList<>(polygon.size());
double boundHeight = bounds.northeast.latitude - bounds.southwest.latitude;
double boundWidth = bounds.northeast.longitude - bounds.southwest.longitude;
for (int i = 0; i < polygon.size(); i++) {
LatLng latLng = polygon.get(i);
Point screenPoint = new Point();
screenPoint.x = (int) (width * (latLng.longitude - bounds.southwest.longitude) / boundWidth);
screenPoint.y = height - (int) (height * (latLng.latitude - bounds.southwest.latitude) / boundHeight);
screenPoints.add(screenPoint);
}
path.moveTo(screenPoints.get(0).x, screenPoints.get(0).y);
for (int i = 1; i < polygon.size(); i++) {
path.lineTo(screenPoints.get(i).x, screenPoints.get(i).y);
}
path.close();
canvas.drawPath(path, paint);
return bitmap;
}
И вы можете использовать его таким образом:
Bitmap tileBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_forest); // create bitmap of fill icon
fillPoly(mGoogleMap.getProjection(), polygon, tileBitmap, 0.5f);
mGoogleMap.getProjection()
необходимо для правильного расчета пропорции растрового изображения. Если вам нужно использовать mGoogleMap.getProjection()
до отображения карты, вы должны использовать это решение andr .
В результате вы получили что-то вроде этого:
Если вам нужно «определенное расстояние между значками», то пробелы должны быть внутри самого значка:
(h - горизонтальное пространство, v - вертикальное пространство).
Наземные наложения всегда масштабируются и поворачиваются вместе с картой, и если вам нужен значок, не зависящий от масштаба, вам следует использовать другой подход. Также для больших оверлеев растровых изображений может не хватать памяти.
3) использовать Tile Overlays
. На самом деле это похоже на стр.2, но наложение растрового изображения генерируется для каждого уровня масштабирования и разбивается на маленькие (например, 256x256 пикселей) плитки, чтобы избежать высокого потребления памяти. Вы можете сохранить сгенерированные тайлы в файловой системе устройства и реализовать TileProvider
как в 'this' ответ Alex Vasilkov
;
4) использовать custom
MapView
на основе вида с переопределением dispatchDraw()
, где вы можете управлять всем рисованием на холсте вида , Это лучшее, но сложное решение.
Внимание! Это только демонстрация, а не полное решение (например, оно не проверялось на отрицательный LatLng и т. Д.).